Skip to content

Commit 29d23e7

Browse files
author
Eimantas
authored
Feature: Identity upgrade (#2)
1 parent 113c142 commit 29d23e7

File tree

7 files changed

+100
-22
lines changed

7 files changed

+100
-22
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
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
@@ -1,6 +1,6 @@
11
{
22
"name": "@reactway/api-builder",
3-
"version": "1.0.0-alpha",
3+
"version": "1.0.0-alpha.2",
44
"description": "An easy api client builder for applications with identity.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/__tests__/api-builder.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiBuilder } from "../api-builder";
22
import fetchMock from "fetch-mock";
3-
import { ApiRequestBinaryBody, LoginResponseDto, HttpMethods, ApiRequest } from "../contracts";
3+
import { ApiRequestBinaryBody, OAuthResponseDto, HttpMethods, ApiRequest } from "../contracts";
44
import { OAuthIdentity } from "../identities/oauth-identity";
55
jest.useFakeTimers();
66

@@ -16,7 +16,7 @@ const TEST_HOST = "https://example.com";
1616
const LOGIN_PATH = "/api/login";
1717
const LOGOUT_PATH = "/api/logout";
1818

19-
const LOGIN_RESPONSE: LoginResponseDto = {
19+
const LOGIN_RESPONSE: OAuthResponseDto = {
2020
scope: "offline_access",
2121
token_type: "Bearer",
2222
access_token: "ACCESS_TOKEN",

src/api-builder.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,22 @@ export class ApiBuilder {
5757
const forceRequestIndex = this.requestsQueue.findIndex(x => x.isForced === true);
5858
const canMakeRequest = this.canMakeRequest();
5959

60-
// FIXME: Refactor to be more readable (fail fast).
60+
if (!canMakeRequest && forceRequestIndex === -1) {
61+
return;
62+
}
63+
6164
// If there are forced requests waiting in the queue.
6265
if (forceRequestIndex !== -1) {
6366
// Perform them first no matter whether we're allowed to make requests.
6467
// Take force request out of the queue.
6568
request = this.requestsQueue.splice(forceRequestIndex, 1)[0];
66-
} else if (canMakeRequest) {
69+
} else {
6770
// Simply take FIFO request.
6871
const nextInQueue = this.requestsQueue.shift();
6972
if (nextInQueue == null) {
7073
return;
7174
}
7275
request = nextInQueue;
73-
} else {
74-
return;
7576
}
7677

7778
// Increment pending requests count.

src/contracts.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,12 @@ export interface OAuthIdentityConfiguration {
8787
tokenRenewalEnabled?: boolean;
8888
}
8989

90-
export interface LoginResponseDto {
91-
scope?: string;
90+
export interface OAuthResponseDto {
9291
token_type: string;
9392
access_token: string;
94-
expires_in: number;
95-
refresh_token: string;
93+
expires_in?: number;
94+
scope?: string;
95+
refresh_token?: string;
96+
id_token?: string;
9697
}
9798
// #endregion

src/identities/__tests__/oauth-identity.test.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
import { OAuthIdentity } from "../oauth-identity";
2-
import { LoginResponseDto, HttpMethods } from "../../contracts";
2+
import { OAuthResponseDto, HttpMethods } from "../../contracts";
33
import fetchMock from "fetch-mock";
44
jest.useFakeTimers();
55

66
const TEST_HOST = "https://example.com";
77
const LOGIN_PATH = "/api/login";
88
const LOGOUT_PATH = "/api/logout";
99

10-
const LOGIN_RESPONSE: LoginResponseDto = {
10+
const LOGIN_RESPONSE: OAuthResponseDto = {
1111
scope: "offline_access",
1212
token_type: "Bearer",
1313
access_token: "ACCESS_TOKEN",
1414
refresh_token: "REFRESH_TOKEN",
1515
// Seconds
1616
expires_in: 28800
1717
};
18+
const LOGIN_RESPONSE_NO_EXPIRES_IN: OAuthResponseDto = {
19+
scope: "offline_access",
20+
token_type: "Bearer",
21+
access_token: "ACCESS_TOKEN",
22+
refresh_token: "REFRESH_TOKEN"
23+
};
24+
const LOGIN_RESPONSE_NO_REFRESH_TOKEN: OAuthResponseDto = {
25+
scope: "offline_access",
26+
token_type: "Bearer",
27+
access_token: "ACCESS_TOKEN",
28+
// Seconds
29+
expires_in: 28800
30+
};
1831

1932
// #region Mocked fetch results.
2033
function mockLoginSuccess(): void {
@@ -26,6 +39,25 @@ function mockLoginSuccess(): void {
2639
})
2740
);
2841
}
42+
function mockLoginSuccessNoExpiresIn(): void {
43+
fetchMock.post(
44+
`${TEST_HOST}${LOGIN_PATH}`,
45+
Promise.resolve({
46+
status: 200,
47+
body: JSON.stringify(LOGIN_RESPONSE_NO_EXPIRES_IN)
48+
})
49+
);
50+
}
51+
52+
function mockLoginSuccessNoRefreshToken(): void {
53+
fetchMock.post(
54+
`${TEST_HOST}${LOGIN_PATH}`,
55+
Promise.resolve({
56+
status: 200,
57+
body: JSON.stringify(LOGIN_RESPONSE_NO_REFRESH_TOKEN)
58+
})
59+
);
60+
}
2961

3062
function mockRenewFailed(): void {
3163
fetchMock.post(
@@ -86,6 +118,38 @@ it("logins successfully", async done => {
86118
done();
87119
});
88120

121+
it("logins successfully with no expires_in property", async done => {
122+
const identity = new OAuthIdentity({
123+
host: TEST_HOST,
124+
loginPath: LOGIN_PATH,
125+
logoutPath: LOGOUT_PATH
126+
});
127+
128+
mockLoginSuccessNoExpiresIn();
129+
try {
130+
await identity.login("", "");
131+
done.fail();
132+
} catch {
133+
done();
134+
}
135+
});
136+
137+
it("logins successfully with no refresh token", async done => {
138+
const fn = jest.fn();
139+
const identity = new OAuthIdentity({
140+
host: TEST_HOST,
141+
loginPath: LOGIN_PATH,
142+
logoutPath: LOGOUT_PATH
143+
});
144+
145+
mockLoginSuccessNoRefreshToken();
146+
identity.on("login", fn);
147+
await identity.login("", "");
148+
149+
expect(fn).toBeCalled();
150+
done();
151+
});
152+
89153
it("logins successfully with disabled renewal token", async done => {
90154
const fn = jest.fn();
91155
const identity = new OAuthIdentity({
@@ -143,7 +207,7 @@ it("logins successfully with time renewal time less than expiration time", async
143207
host: TEST_HOST,
144208
loginPath: LOGIN_PATH,
145209
logoutPath: LOGOUT_PATH,
146-
renewTokenTime: LOGIN_RESPONSE.expires_in + 100
210+
renewTokenTime: 28900
147211
});
148212

149213
mockLoginSuccess();
@@ -167,7 +231,7 @@ it("logins successfully and new token", async done => {
167231
await identity.login("", "");
168232
expect(fn).toBeCalled();
169233
jest.runAllTimers();
170-
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), LOGIN_RESPONSE.expires_in - 120);
234+
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 28680);
171235
done();
172236
});
173237

src/identities/oauth-identity.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
QueuedRequest,
99
OAuthIdentityConfiguration,
1010
HttpMethods,
11-
LoginResponseDto
11+
OAuthResponseDto
1212
} from "../contracts";
1313

1414
const IdentityEventEmitter: { new (): StrictEventEmitter<EventEmitter, IdentityMechanismEvents> } = EventEmitter;
@@ -17,7 +17,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
1717
super();
1818
}
1919

20-
private loginData: LoginResponseDto | undefined;
20+
private loginData: OAuthResponseDto | undefined;
2121
private renewalTimeoutId: number | undefined;
2222
/**
2323
* Value is set in seconds.
@@ -44,7 +44,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
4444
}
4545

4646
this.emit("login");
47-
this.setLoginData((await response.json()) as LoginResponseDto);
47+
this.setLoginData((await response.json()) as OAuthResponseDto);
4848
}
4949

5050
public async logout(): Promise<void> {
@@ -111,12 +111,24 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
111111
throw new Error("Failed renew token.");
112112
}
113113

114-
this.setLoginData((await response.json()) as LoginResponseDto);
114+
this.setLoginData((await response.json()) as OAuthResponseDto);
115115
}
116116

117-
private setLoginData(loginData: LoginResponseDto): void {
117+
private setLoginData(loginData: OAuthResponseDto): void {
118+
if (loginData.expires_in == null) {
119+
throw Error("Not supported without expiration time.");
120+
}
121+
118122
this.loginData = loginData;
119123

124+
// If response do not have `refresh_token` we are not using renewal mechanism.
125+
if (loginData.refresh_token == null) {
126+
return;
127+
}
128+
129+
const refreshToken = loginData.refresh_token;
130+
131+
// If response has `refresh_token` but we do not want to use renewal mechanism.
120132
if (this.configuration.tokenRenewalEnabled === false) {
121133
return;
122134
}
@@ -127,7 +139,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
127139
}
128140

129141
const timeoutNumber = this.renewalTime(loginData.expires_in);
130-
this.renewalTimeoutId = window.setTimeout(() => this.renewToken(loginData.refresh_token), timeoutNumber);
142+
this.renewalTimeoutId = window.setTimeout(() => this.renewToken(refreshToken), timeoutNumber);
131143
}
132144

133145
private renewalTime(time: number): number {

0 commit comments

Comments
 (0)