Skip to content

Commit

Permalink
feat!: allow body reuse (#92)
Browse files Browse the repository at this point in the history
* feat!: allow re-use of response json

* feat!: safer typing

* chore: fix imports

* chore: remove old type

* chore: remove unneeded casts

* chore: npm audit fix

* chore: fix dependabot error

* feat!: actually fix the clone issue this time

* chore: fix table-data-client type args

BREAKING CHANGE: api client now returns Response instead of BugSplatResponse to allow cloning. BugSplatResponse `json` no longer returns type `any`.
  • Loading branch information
bobbyg603 authored Jan 9, 2023
1 parent ec5c3fc commit 498f448
Show file tree
Hide file tree
Showing 22 changed files with 248 additions and 159 deletions.
62 changes: 24 additions & 38 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"jasmine-reporters": "^2.4.0",
"semantic-release": "^19.0.2",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.11.0",
"tsconfig-paths": "^4.1.2",
"tsconfig-replace-paths": "^0.0.5",
"typescript": "^4.4.3"
}
Expand Down
55 changes: 46 additions & 9 deletions spec/fakes/common/response.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
export function createFakeResponseBody(
status: number,
json: any = {},
export function createFakeResponseBody<T>(
status = 200,
json = {} as T,
ok = true,
headers: any = []
) {
return {
headers = new Map()
): FakeResponseBody<T> {
return new FakeResponseBody(
status,
json,
ok,
json: async() => (json),
headers: { get: () => headers }
};
headers
);
}

export class FakeResponseBody<T> {

private _json: T;
private _headers: Map<string, string>;

get headers(): Map<string, string> {
return this._headers;
}

constructor(
public readonly status = 200,
json = {} as T,
public readonly ok = true,
headers = new Map() as Map<string,string>
) {
this._json = json;
this._headers = headers;
}

async json(): Promise<T> {
return this._json;
}

async text(): Promise<string> {
return JSON.stringify(this._json);
}

clone(): FakeResponseBody<T> {
return new FakeResponseBody<T>(
this.status,
this._json,
this.ok,
this._headers
);
}
}
7 changes: 4 additions & 3 deletions src/common/client/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export interface ApiClient {
createFormData(): FormData;
fetch(route: string, init?: RequestInit): Promise<BugSplatResponse>;
fetch<T>(route: string, init?: RequestInit): Promise<BugSplatResponse<T>>;
}

export interface BugSplatResponse {
export interface BugSplatResponse<T = unknown> {
status: number;
json: () => Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
json: () => Promise<T>;
text: () => Promise<string>;
}
17 changes: 10 additions & 7 deletions src/common/client/bugsplat-api-client/bugsplat-api-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ describe('BugSplatApiClient', () => {
let expectedStatus;
let expectedJson;
let fakeFormData;
let fakeSuccessReponseBody;
let fakeSuccessResponseBody;

beforeEach(() => {
const headers = new Map([['set-cookie', cookie]]);
appendSpy = jasmine.createSpy();
expectedStatus = 'success';
expectedJson = { success: 'true' };
fakeFormData = { append: appendSpy, toString: () => 'BugSplat rocks!' };
fakeSuccessReponseBody = createFakeResponseBody(expectedStatus, expectedJson, true, cookie);
fakeSuccessResponseBody = createFakeResponseBody(expectedStatus, expectedJson, true, headers);
client = createFakeBugSplatApiClient(
Environment.Node,
fakeSuccessReponseBody,
fakeSuccessResponseBody,
fakeFormData
);
});
Expand Down Expand Up @@ -52,7 +53,7 @@ describe('BugSplatApiClient', () => {
it('should call fetch with include credentials in request init', async () => {
client = createFakeBugSplatApiClient(
Environment.WebBrowser,
fakeSuccessReponseBody,
fakeSuccessResponseBody,
fakeFormData
);

Expand Down Expand Up @@ -84,8 +85,10 @@ describe('BugSplatApiClient', () => {
});
});

it('should return result', () => {
expect(result).toEqual(fakeSuccessReponseBody);
it('should return result', async () => {
const expectedJson = await fakeSuccessResponseBody.json();
const resultJson = await result.json();
expect(resultJson).toEqual(jasmine.objectContaining(expectedJson as Record<string, unknown>));
});
});

Expand Down Expand Up @@ -117,7 +120,7 @@ describe('BugSplatApiClient', () => {
});

it('should return result', () => {
expect(result).toEqual(fakeSuccessReponseBody);
expect(result).toEqual(fakeSuccessResponseBody);
});

describe('error', () => {
Expand Down
19 changes: 14 additions & 5 deletions src/common/client/bugsplat-api-client/bugsplat-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApiClient, bugsplatAppHostUrl, BugSplatResponse, Environment } from '@common';
import { BugSplatLoginResponse } from './bugsplat-login-response';

export class BugSplatApiClient implements ApiClient {
private _createFormData = () => new FormData();
Expand Down Expand Up @@ -34,7 +35,7 @@ export class BugSplatApiClient implements ApiClient {
return this._createFormData();
}

async fetch(route: string, init: RequestInit = {}): Promise<BugSplatResponse> {
async fetch<T>(route: string, init: RequestInit = {}): Promise<BugSplatResponse<T>> {
if (!init.headers) {
init.headers = {};
}
Expand All @@ -48,21 +49,29 @@ export class BugSplatApiClient implements ApiClient {
}

const url = new URL(route, this._host);
return this._fetch(url.href, init);
const response = await this._fetch(url.href, init);
const status = response.status;

return {
status,
json: async () => response.clone().json(),
text: async () => response.clone().text()
};
}

async login(email: string, password: string): Promise<BugSplatResponse> {
async login(email: string, password: string): Promise<BugSplatResponse<BugSplatLoginResponse>> {
const url = new URL('/api/authenticatev3', this._host);
const formData = this._createFormData();
formData.append('email', email);
formData.append('password', password);
formData.append('Login', 'Login');
const response = await this._fetch(url.href, <RequestInit><unknown>{
const request = {
method: 'POST',
body: formData,
cache: 'no-cache',
redirect: 'follow'
});
} as RequestInit;
const response = await this._fetch(url.href, request);

if (response.status === 401) {
throw new Error('Could not authenticate, check credentials and try again');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface BugSplatLoginResponse {
user: string;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { createFakeFormData } from '@spec/fakes/common/form-data';
import { createFakeResponseBody } from '@spec/fakes/common/response';
import { createFakeResponseBody, FakeResponseBody } from '@spec/fakes/common/response';
import { BugSplatResponse } from 'dist/esm';
import { OAuthLoginResponse } from 'dist/esm/common/client/oauth-client-credentials-api-client/oauth-login-response';
import { OAuthClientCredentialsClient } from './oauth-client-credentials-api-client';

describe('OAuthClientCredentialsClient', () => {
let clientId;
let clientSecret;
let fakeAuthorizeResponseBody;
let fakeAuthorizeResult;
let fakeFetchResponseBody;
let clientId: string;
let clientSecret: string;
let fakeAuthorizeResponseBody: FakeResponseBody<AuthorizeResult>;
let fakeAuthorizeResult: AuthorizeResult;
let fakeFetchResponseBody: FakeResponseBody<unknown>;
let fakeFetchResult;
let fakeFormData;
let host;
Expand Down Expand Up @@ -38,7 +40,7 @@ describe('OAuthClientCredentialsClient', () => {
});

describe('login', () => {
let result;
let result: BugSplatResponse<OAuthLoginResponse>;

beforeEach(async () => result = await sut.login());

Expand Down Expand Up @@ -70,7 +72,7 @@ describe('OAuthClientCredentialsClient', () => {
});

describe('error', () => {
it('should return useful error message when authenication fails', async () => {
it('should return useful error message when authentication fails', async () => {
const failureResponseBody = createFakeResponseBody(200, { error: 'invalid_client' });
sut = createFakeOAuthClientCredentialsClient(
'blah',
Expand All @@ -91,7 +93,7 @@ describe('OAuthClientCredentialsClient', () => {
describe('fetch', () => {
let route;
let headers;
let result;
let result: BugSplatResponse<unknown>;

beforeEach(async () => {
route = '/what/will/we/do/with/a/drunken/sailor';
Expand Down Expand Up @@ -129,8 +131,10 @@ describe('OAuthClientCredentialsClient', () => {
}));
});

it('should return result', () => {
expect(result).toEqual(fakeFetchResponseBody);
it('should return result', async () => {
const expectedJson = await fakeFetchResponseBody.json();
const resultJson = await result.json();
expect(resultJson).toEqual(jasmine.objectContaining(expectedJson as Record<string, unknown>));
});

describe('error', () => {
Expand Down Expand Up @@ -159,3 +163,8 @@ function createFakeOAuthClientCredentialsClient(
(<any>client)._createFormData = () => formData;
return client;
}

interface AuthorizeResult {
access_token: string;
token_type: string;
}
Loading

0 comments on commit 498f448

Please sign in to comment.