Skip to content

Commit

Permalink
OAuthClient test refactor + RESTClient tests
Browse files Browse the repository at this point in the history
  • Loading branch information
hrax committed Aug 3, 2024
1 parent bd770b7 commit 7e27a31
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 44 deletions.
4 changes: 2 additions & 2 deletions modules/core/OAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class OAuthUsernamePasswordIncorrect extends Error {
}
};

export default class OAuthClient {
export class OAuthClient {
private config: InstanceConfig;

// create Server for code?
Expand Down Expand Up @@ -218,6 +218,6 @@ export default class OAuthClient {
if (options.headers == null) {
options.headers = {};
}
options.headers.authorization = `Bearer ${this.config.auth.token!.access_token}`;
options.headers.authorization = `${this.config.auth.token!.token_type} ${this.config.auth.token!.access_token}`;
}
}
53 changes: 27 additions & 26 deletions modules/core/RESTClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { RequestOptions } from "https";
import { URLSearchParams } from "url";
import OAuthClient from "./OAuthClient";
import { OAuthClient } from "./OAuthClient";
import { Request, Response } from "./Request";
import pkg from "../../package.json";

export enum RESPONSE_STATUS {
OK = 200,
Expand All @@ -11,20 +12,20 @@ export enum RESPONSE_STATUS {
ERROR = 500
};

enum TableAPI {
export enum TableAPI {
UPDATE_XML_PATH = "/api/now/table/sys_update_xml",
UPDATE_SET_PATH = "/api/now/table/sys_update_set",
DICTIONARY_PATH = "/api/now/table/sys_dictionary",
DB_OBJECT_PATH = "/api/now/table/sys_db_object",
USER_PREFERENCE_PATH = "/api/now/table/sys_user_preference"
}

interface TableFieldData {
export interface TableFieldData {
name: string;
element: string;
}

interface TableParentData {
export interface TableParentData {
name: string;
"super_class.name": string;
}
Expand All @@ -34,7 +35,7 @@ export interface RESTResponse<T = any> {
}

export class RESTClient {
static readonly NO_PREFERENCE = "No preference";
static readonly NO_TABLE_CONFIG_PREF = "No Table Config preference!";
private instance: InstanceConfig;
private oauthClient: OAuthClient;

Expand Down Expand Up @@ -85,12 +86,12 @@ export class RESTClient {
requestUpdateXMLByUpdateSetIds
*/

private async getTableParentData(): Promise<RESTResponse<TableParentData>> {
async getTableParentData(): Promise<Array<TableParentData>> {
const url: URL = new URL(TableAPI.DB_OBJECT_PATH, this.instance.baseUrl);
url.searchParams.set("sysparm_fields", "sys_id");
url.searchParams.set("sysparm_limit", "1");
url.searchParams.set("sysparm_no_count", "true");
url.searchParams.set("sysparm_suppress_pagination_header", "true");
url.searchParams.set("sysparm_fields", "name,super_class.name");
url.searchParams.set("sysparm_query", "nameBETWEEN @varz^ORnameBETWEENvas@wfz^ORnameBETWEENwg@~^super_class.name!=sys_metadata^ORDERBYname");

const options: RequestOptions = {
method: "GET",
Expand All @@ -102,25 +103,24 @@ export class RESTClient {

await this.oauthClient.handleAuthentication(options);

return await Request.json(url, options)
return (<RESTResponse<TableParentData>>await Request.json(url, options)
.catch((reason: any) => {
if (reason instanceof Response) {
const response: Response = reason;
if (!response.isEmpty() && response.isUnauthorized()) {
return Promise.reject("Unauthorized");
return Promise.reject(response);
}
}
return Promise.reject(reason);
});
})).result;
}

/**
* Load table field data
* Skips tables whos name starts with wf_ or var_
*/
private async getTableFieldData(): Promise<RESTResponse<TableFieldData>> {
async getTableFieldData(): Promise<Array<TableFieldData>> {
const url: URL = new URL(TableAPI.DICTIONARY_PATH, this.instance.baseUrl);
url.searchParams.set("sysparm_fields", "sys_id");
url.searchParams.set("sysparm_no_count", "true");
url.searchParams.set("sysparm_suppress_pagination_header", "true");
url.searchParams.set("sysparm_fields", "name,element");
Expand All @@ -136,26 +136,26 @@ export class RESTClient {

await this.oauthClient.handleAuthentication(options);

return await Request.json(url, options)
return (<RESTResponse<TableFieldData>>await Request.json(url, options)
.catch((reason: any) => {
if (reason instanceof Response) {
const response: Response = reason;
if (!response.isEmpty() && response.isUnauthorized()) {
return Promise.reject("Unauthorized");
return Promise.reject(response);
}
}
return Promise.reject(reason);
});
})).result;
}

private async getTableConfigurationPreference(): Promise<TableConfig> {
const url: URL = new URL(TableAPI.DICTIONARY_PATH, this.instance.baseUrl);
url.searchParams.set("sysparm_fields", "sys_id");
async getTableConfigurationPreference(): Promise<TableConfig> {
const prefName = `${pkg.name}/table_config`;
const url: URL = new URL(TableAPI.USER_PREFERENCE_PATH, this.instance.baseUrl);
url.searchParams.set("sysparm_no_count", "true");
url.searchParams.set("sysparm_suppress_pagination_header", "true");
url.searchParams.set("sysparm_limit", "2");
url.searchParams.set("sysparm_fields", "name,value");
url.searchParams.set("sysparm_query", "nameBETWEEN @varz^ORnameBETWEENvas@wfz^ORnameBETWEENwg@~^internal_type=script^ORinternal_type=script_plain^ORinternal_type=script_server^GROUPBYname^ORDERBYelement");
url.searchParams.set("sysparm_query", `name=${prefName}^userISEMPTY^ORuserDYNAMIC90d1921e5f510100a9ad2572f2b477fe^ORDERBYDESCuser`);

const options: RequestOptions = {
method: "GET",
Expand All @@ -172,16 +172,17 @@ export class RESTClient {
if (reason instanceof Response) {
const response: Response = reason;
if (!response.isEmpty() && response.isUnauthorized()) {
return Promise.reject("Unauthorized");
return Promise.reject(response);
}
}
return Promise.reject(reason);
});

if (response.result.length === 0) {
return Promise.reject(RESTClient.NO_PREFERENCE);
return Promise.reject(RESTClient.NO_TABLE_CONFIG_PREF);
}

// There should be always at least 1, since we are loading 2
return response.result[0];
}

Expand All @@ -199,15 +200,15 @@ export class RESTClient {
};

// Load Table + Table Parent data
const tables = (await this.getTableParentData()).result.reduce((accumulator, value) => {
const tables = (await this.getTableParentData()).reduce((accumulator, value) => {
accumulator[value.name] = value;
return accumulator;
}, <{[ key: string]: TableParentData}>{});
// Load Table + Table field data
const fields: RESTResponse<TableFieldData> = await this.getTableFieldData();
const fields: Array<TableFieldData> = await this.getTableFieldData();

// Process all loaded fields
fields.result.forEach((data) => {
fields.forEach((data) => {
// If table has not been processed yet
if (config.tables[data.name] == null) {
config.tables[data.name] = {
Expand Down Expand Up @@ -237,7 +238,7 @@ export class RESTClient {
// Load table configuration from user preference
let pref: TableConfig = await this.getTableConfigurationPreference()
.catch((async (reason) => {
if (reason === RESTClient.NO_PREFERENCE) {
if (reason === RESTClient.NO_TABLE_CONFIG_PREF) {
return await this.setupTableConfiguration();
}
return Promise.reject(reason);
Expand Down
46 changes: 30 additions & 16 deletions spec/coreSpec/OAuthClientSpec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import exp from "constants";
import OAuthClient, { OAuthCodeExpired, OAuthRefreshTokenExpired, OAuthUsernamePasswordIncorrect } from "../../modules/core/OAuthClient";
import { OAuthClient, OAuthCodeExpired, OAuthRefreshTokenExpired, OAuthUsernamePasswordIncorrect } from "../../modules/core/OAuthClient";
import { Request, Response } from "../../modules/core/Request";
import { IncomingMessage } from "http";
import { Socket } from "net";
import { RequestOptions } from "https";

describe("OAuthClient", () => {
describe("OAuthClientSpec", () => {
const config: InstanceConfig = {
name: "test",
baseUrl: "https://example.com",
Expand All @@ -24,8 +25,8 @@ describe("OAuthClient", () => {
}
};

describe("#isTokenExpired", () => {
it("token expired if token null", () => {
describe("token validation", () => {
it("should expire if token is null", () => {
const token: InstanceOAuthTokenData = {
clientID: "clientID",
clientSecret: "clientSecret",
Expand All @@ -34,7 +35,7 @@ describe("OAuthClient", () => {

expect(OAuthClient.isTokenExpired(token)).toBeTrue();
});
it("token expired", () => {
it("should expire", () => {
const token: InstanceOAuthTokenData = {
clientID: "clientID",
clientSecret: "clientSecret",
Expand All @@ -52,7 +53,7 @@ describe("OAuthClient", () => {

expect(OAuthClient.isTokenExpired(token)).toBeTrue();
});
it("token valid", () => {
it("should be valid", () => {
const tokenValid: InstanceOAuthTokenData = {
clientID: "clientID",
clientSecret: "clientSecret",
Expand All @@ -72,16 +73,16 @@ describe("OAuthClient", () => {
});
});

describe("#requestTokenByUsername", () => {
it("success", async () => {
describe("request token by username", () => {
it("should resolve on 200", async () => {
let response = Response.empty(JSON.stringify(config.auth.token!));
spyOn(response, "isOK").and.returnValue(true);
spyOn(Request, "execute").and.resolveTo(response);
const client = new OAuthClient(config);
await expectAsync(client.requestTokenByUsername("admin", "admin")).toBeResolvedTo(config.auth.token!);
});

it("unauthorized", async () => {
it("should reject on 401", async () => {
let response = Response.empty(JSON.stringify({}));
spyOn(response, "isEmpty").and.returnValue(false);
spyOn(response, "isUnauthorized").and.returnValue(true);
Expand All @@ -91,17 +92,17 @@ describe("OAuthClient", () => {
});
});

describe("#requestTokenByCode", () => {
describe("request token by code", () => {
const code = "1234";
it("success", async () => {
it("should resolve on 200", async () => {
let response = Response.empty(JSON.stringify(config.auth.token!));
spyOn(response, "isOK").and.returnValue(true);
spyOn(Request, "execute").and.resolveTo(response);
const client = new OAuthClient(config);
await expectAsync(client.requestTokenByCode(code)).toBeResolvedTo(config.auth.token!);
});

it("unauthorized", async () => {
it("should reject on 401", async () => {
let response = Response.empty("{}");
spyOn(response, "isEmpty").and.returnValue(false);
spyOn(response, "isUnauthorized").and.returnValue(true);
Expand All @@ -111,16 +112,16 @@ describe("OAuthClient", () => {
});
});

describe("#refreshToken", () => {
it("success", async () => {
describe("refresh token", () => {
it("should resolve on 200", async () => {
let response = Response.empty(JSON.stringify(config.auth.token!));
spyOn(response, "isOK").and.returnValue(true);
spyOn(response, "isOK").and.returnValue(true);
spyOn(Request, "execute").and.resolveTo(response);
const client = new OAuthClient(config);
await expectAsync(client.refreshToken()).toBeResolvedTo(config.auth.token!);
});

it("unauthorized", async () => {
it("should reject on 401", async () => {
let response = Response.empty("{}");
spyOn(response, "isEmpty").and.returnValue(false);
spyOn(response, "isUnauthorized").and.returnValue(true);
Expand All @@ -129,4 +130,17 @@ describe("OAuthClient", () => {
await expectAsync(client.refreshToken()).toBeRejectedWith(new OAuthRefreshTokenExpired(response.data));
});
});

it("should extend headers with authentication", async() => {
const token: SNOAuthToken = config.auth.token!;
const options: RequestOptions = {
method: "GET"
};

const client = new OAuthClient(config);
await client.handleAuthentication(options);

expect(options.headers).not.toBeUndefined();
expect(options.headers!.authorization).toBe(`${token.token_type} ${token.access_token}`);
});
})
Loading

0 comments on commit 7e27a31

Please sign in to comment.