Skip to content

Commit 498f448

Browse files
authored
feat!: allow body reuse (#92)
* 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`.
1 parent ec5c3fc commit 498f448

File tree

22 files changed

+248
-159
lines changed

22 files changed

+248
-159
lines changed

package-lock.json

Lines changed: 24 additions & 38 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"jasmine-reporters": "^2.4.0",
7070
"semantic-release": "^19.0.2",
7171
"ts-node": "^10.0.0",
72-
"tsconfig-paths": "^3.11.0",
72+
"tsconfig-paths": "^4.1.2",
7373
"tsconfig-replace-paths": "^0.0.5",
7474
"typescript": "^4.4.3"
7575
}

spec/fakes/common/response.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,50 @@
1-
export function createFakeResponseBody(
2-
status: number,
3-
json: any = {},
1+
export function createFakeResponseBody<T>(
2+
status = 200,
3+
json = {} as T,
44
ok = true,
5-
headers: any = []
6-
) {
7-
return {
5+
headers = new Map()
6+
): FakeResponseBody<T> {
7+
return new FakeResponseBody(
88
status,
9+
json,
910
ok,
10-
json: async() => (json),
11-
headers: { get: () => headers }
12-
};
11+
headers
12+
);
13+
}
14+
15+
export class FakeResponseBody<T> {
16+
17+
private _json: T;
18+
private _headers: Map<string, string>;
19+
20+
get headers(): Map<string, string> {
21+
return this._headers;
22+
}
23+
24+
constructor(
25+
public readonly status = 200,
26+
json = {} as T,
27+
public readonly ok = true,
28+
headers = new Map() as Map<string,string>
29+
) {
30+
this._json = json;
31+
this._headers = headers;
32+
}
33+
34+
async json(): Promise<T> {
35+
return this._json;
36+
}
37+
38+
async text(): Promise<string> {
39+
return JSON.stringify(this._json);
40+
}
41+
42+
clone(): FakeResponseBody<T> {
43+
return new FakeResponseBody<T>(
44+
this.status,
45+
this._json,
46+
this.ok,
47+
this._headers
48+
);
49+
}
1350
}

src/common/client/api-client.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
export interface ApiClient {
22
createFormData(): FormData;
3-
fetch(route: string, init?: RequestInit): Promise<BugSplatResponse>;
3+
fetch<T>(route: string, init?: RequestInit): Promise<BugSplatResponse<T>>;
44
}
55

6-
export interface BugSplatResponse {
6+
export interface BugSplatResponse<T = unknown> {
77
status: number;
8-
json: () => Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
8+
json: () => Promise<T>;
9+
text: () => Promise<string>;
910
}

src/common/client/bugsplat-api-client/bugsplat-api-client.spec.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@ describe('BugSplatApiClient', () => {
1414
let expectedStatus;
1515
let expectedJson;
1616
let fakeFormData;
17-
let fakeSuccessReponseBody;
17+
let fakeSuccessResponseBody;
1818

1919
beforeEach(() => {
20+
const headers = new Map([['set-cookie', cookie]]);
2021
appendSpy = jasmine.createSpy();
2122
expectedStatus = 'success';
2223
expectedJson = { success: 'true' };
2324
fakeFormData = { append: appendSpy, toString: () => 'BugSplat rocks!' };
24-
fakeSuccessReponseBody = createFakeResponseBody(expectedStatus, expectedJson, true, cookie);
25+
fakeSuccessResponseBody = createFakeResponseBody(expectedStatus, expectedJson, true, headers);
2526
client = createFakeBugSplatApiClient(
2627
Environment.Node,
27-
fakeSuccessReponseBody,
28+
fakeSuccessResponseBody,
2829
fakeFormData
2930
);
3031
});
@@ -52,7 +53,7 @@ describe('BugSplatApiClient', () => {
5253
it('should call fetch with include credentials in request init', async () => {
5354
client = createFakeBugSplatApiClient(
5455
Environment.WebBrowser,
55-
fakeSuccessReponseBody,
56+
fakeSuccessResponseBody,
5657
fakeFormData
5758
);
5859

@@ -84,8 +85,10 @@ describe('BugSplatApiClient', () => {
8485
});
8586
});
8687

87-
it('should return result', () => {
88-
expect(result).toEqual(fakeSuccessReponseBody);
88+
it('should return result', async () => {
89+
const expectedJson = await fakeSuccessResponseBody.json();
90+
const resultJson = await result.json();
91+
expect(resultJson).toEqual(jasmine.objectContaining(expectedJson as Record<string, unknown>));
8992
});
9093
});
9194

@@ -117,7 +120,7 @@ describe('BugSplatApiClient', () => {
117120
});
118121

119122
it('should return result', () => {
120-
expect(result).toEqual(fakeSuccessReponseBody);
123+
expect(result).toEqual(fakeSuccessResponseBody);
121124
});
122125

123126
describe('error', () => {

src/common/client/bugsplat-api-client/bugsplat-api-client.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApiClient, bugsplatAppHostUrl, BugSplatResponse, Environment } from '@common';
2+
import { BugSplatLoginResponse } from './bugsplat-login-response';
23

34
export class BugSplatApiClient implements ApiClient {
45
private _createFormData = () => new FormData();
@@ -34,7 +35,7 @@ export class BugSplatApiClient implements ApiClient {
3435
return this._createFormData();
3536
}
3637

37-
async fetch(route: string, init: RequestInit = {}): Promise<BugSplatResponse> {
38+
async fetch<T>(route: string, init: RequestInit = {}): Promise<BugSplatResponse<T>> {
3839
if (!init.headers) {
3940
init.headers = {};
4041
}
@@ -48,21 +49,29 @@ export class BugSplatApiClient implements ApiClient {
4849
}
4950

5051
const url = new URL(route, this._host);
51-
return this._fetch(url.href, init);
52+
const response = await this._fetch(url.href, init);
53+
const status = response.status;
54+
55+
return {
56+
status,
57+
json: async () => response.clone().json(),
58+
text: async () => response.clone().text()
59+
};
5260
}
5361

54-
async login(email: string, password: string): Promise<BugSplatResponse> {
62+
async login(email: string, password: string): Promise<BugSplatResponse<BugSplatLoginResponse>> {
5563
const url = new URL('/api/authenticatev3', this._host);
5664
const formData = this._createFormData();
5765
formData.append('email', email);
5866
formData.append('password', password);
5967
formData.append('Login', 'Login');
60-
const response = await this._fetch(url.href, <RequestInit><unknown>{
68+
const request = {
6169
method: 'POST',
6270
body: formData,
6371
cache: 'no-cache',
6472
redirect: 'follow'
65-
});
73+
} as RequestInit;
74+
const response = await this._fetch(url.href, request);
6675

6776
if (response.status === 401) {
6877
throw new Error('Could not authenticate, check credentials and try again');
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface BugSplatLoginResponse {
2+
user: string;
3+
}

src/common/client/oauth-client-credentials-api-client/oauth-client-credentials-api-client.spec.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { createFakeFormData } from '@spec/fakes/common/form-data';
2-
import { createFakeResponseBody } from '@spec/fakes/common/response';
2+
import { createFakeResponseBody, FakeResponseBody } from '@spec/fakes/common/response';
3+
import { BugSplatResponse } from 'dist/esm';
4+
import { OAuthLoginResponse } from 'dist/esm/common/client/oauth-client-credentials-api-client/oauth-login-response';
35
import { OAuthClientCredentialsClient } from './oauth-client-credentials-api-client';
46

57
describe('OAuthClientCredentialsClient', () => {
6-
let clientId;
7-
let clientSecret;
8-
let fakeAuthorizeResponseBody;
9-
let fakeAuthorizeResult;
10-
let fakeFetchResponseBody;
8+
let clientId: string;
9+
let clientSecret: string;
10+
let fakeAuthorizeResponseBody: FakeResponseBody<AuthorizeResult>;
11+
let fakeAuthorizeResult: AuthorizeResult;
12+
let fakeFetchResponseBody: FakeResponseBody<unknown>;
1113
let fakeFetchResult;
1214
let fakeFormData;
1315
let host;
@@ -38,7 +40,7 @@ describe('OAuthClientCredentialsClient', () => {
3840
});
3941

4042
describe('login', () => {
41-
let result;
43+
let result: BugSplatResponse<OAuthLoginResponse>;
4244

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

@@ -70,7 +72,7 @@ describe('OAuthClientCredentialsClient', () => {
7072
});
7173

7274
describe('error', () => {
73-
it('should return useful error message when authenication fails', async () => {
75+
it('should return useful error message when authentication fails', async () => {
7476
const failureResponseBody = createFakeResponseBody(200, { error: 'invalid_client' });
7577
sut = createFakeOAuthClientCredentialsClient(
7678
'blah',
@@ -91,7 +93,7 @@ describe('OAuthClientCredentialsClient', () => {
9193
describe('fetch', () => {
9294
let route;
9395
let headers;
94-
let result;
96+
let result: BugSplatResponse<unknown>;
9597

9698
beforeEach(async () => {
9799
route = '/what/will/we/do/with/a/drunken/sailor';
@@ -129,8 +131,10 @@ describe('OAuthClientCredentialsClient', () => {
129131
}));
130132
});
131133

132-
it('should return result', () => {
133-
expect(result).toEqual(fakeFetchResponseBody);
134+
it('should return result', async () => {
135+
const expectedJson = await fakeFetchResponseBody.json();
136+
const resultJson = await result.json();
137+
expect(resultJson).toEqual(jasmine.objectContaining(expectedJson as Record<string, unknown>));
134138
});
135139

136140
describe('error', () => {
@@ -159,3 +163,8 @@ function createFakeOAuthClientCredentialsClient(
159163
(<any>client)._createFormData = () => formData;
160164
return client;
161165
}
166+
167+
interface AuthorizeResult {
168+
access_token: string;
169+
token_type: string;
170+
}

0 commit comments

Comments
 (0)