Skip to content
This repository was archived by the owner on Dec 3, 2025. It is now read-only.

Commit 1272a56

Browse files
author
Eimantas
authored
Feature: added localstorage (#3)
1 parent 29d23e7 commit 1272a56

File tree

9 files changed

+5540
-5354
lines changed

9 files changed

+5540
-5354
lines changed

config/jest/local-storage-mock.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export class LocalStorageMock implements Storage {
2+
private store: { [key: string]: string } = {};
3+
[name: string]: unknown;
4+
5+
public key(index: number): string | null {
6+
throw new Error("Method not implemented.");
7+
}
8+
9+
public clear(): void {
10+
this.store = {};
11+
}
12+
13+
public getItem(key: string): string | null {
14+
return this.store[key] || null;
15+
}
16+
17+
public setItem(key: string, value: string): void {
18+
this.store[key] = value;
19+
}
20+
21+
public removeItem(key: string): void {
22+
delete this.store[key];
23+
}
24+
25+
public get length(): number {
26+
return Object.keys(this.store).length;
27+
}
28+
}
29+
30+
Object.defineProperty(global, "localStorage", { value: new LocalStorageMock() });

package-lock.json

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

package.json

Lines changed: 7 additions & 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.2",
3+
"version": "1.0.0-alpha.3",
44
"description": "An easy api client builder for applications with identity.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -26,6 +26,8 @@
2626
"Dovydas Navickas <dovydas@quatrodev.com> (https://github.com/DovydasNavickas)",
2727
"Eimantas Dumšė <eimantas@quatrodev.com> (https://github.com/EimantasDumse)"
2828
],
29+
"repository": "github:reactway/api-builder",
30+
"homepage": "https://github.com/reactway/api-builder",
2931
"license": "MIT",
3032
"devDependencies": {
3133
"@types/fetch-mock": "^7.2.3",
@@ -35,6 +37,7 @@
3537
"istanbul-azure-reporter": "^0.1.4",
3638
"jest": "^24.5.0",
3739
"jest-junit": "^6.3.0",
40+
"jest-localstorage-mock": "^2.4.0",
3841
"node-fetch": "^2.3.0",
3942
"simplr-tslint": "^1.0.0-alpha.14",
4043
"ts-jest": "^24.0.0",
@@ -54,6 +57,9 @@
5457
"default",
5558
"jest-junit"
5659
],
60+
"setupFiles": [
61+
"./config/jest/local-storage-mock.ts"
62+
],
5763
"collectCoverage": true,
5864
"testRegex": "/__tests__/.*\\.(test|spec).(ts|tsx)$",
5965
"collectCoverageFrom": [

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export const REQUEST_QUEUE_LIMIT = 5;
2+
export const STORAGE_OAUTH_KEY = "OAuth";

src/contracts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export interface OAuthIdentityConfiguration {
8585
headers?: { [index: string]: string };
8686
renewTokenTime?: number | ((time: number) => number);
8787
tokenRenewalEnabled?: boolean;
88+
storageKey?: string;
89+
storage?: Storage;
8890
}
8991

9092
export interface OAuthResponseDto {

src/helpers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { ApiRequestBinaryBody } from "./contracts";
1+
import { ApiRequestBinaryBody, OAuthResponseDto } from "./contracts";
22

33
// tslint:disable-next-line:no-any
44
export function isBinaryBody(body: any): body is ApiRequestBinaryBody {
55
return body != null && (body as ApiRequestBinaryBody).isBinary === true;
66
}
7+
8+
// tslint:disable-next-line:no-any
9+
export function isOAuthResponse(value: any): value is OAuthResponseDto {
10+
return value != null && value.token_type != null && value.access_token != null;
11+
}

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { OAuthIdentity } from "../oauth-identity";
22
import { OAuthResponseDto, HttpMethods } from "../../contracts";
33
import fetchMock from "fetch-mock";
4+
import { STORAGE_OAUTH_KEY } from "../../constants";
45
jest.useFakeTimers();
56

67
const TEST_HOST = "https://example.com";
@@ -100,6 +101,7 @@ function mockLogoutFailed(): void {
100101

101102
afterEach(() => {
102103
fetchMock.restore();
104+
localStorage.clear();
103105
});
104106

105107
it("logins successfully", async done => {
@@ -348,3 +350,107 @@ it("no Login data is set yet", async done => {
348350
done();
349351
}
350352
});
353+
// LocalStorage
354+
it("logins successfully and localStorage filled with data", async done => {
355+
const fn = jest.fn();
356+
const identity = new OAuthIdentity({
357+
host: TEST_HOST,
358+
loginPath: LOGIN_PATH,
359+
logoutPath: LOGOUT_PATH,
360+
storage: localStorage
361+
});
362+
363+
mockLoginSuccess();
364+
identity.on("login", fn);
365+
await identity.login("", "");
366+
367+
expect(fn).toBeCalled();
368+
expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(JSON.stringify(LOGIN_RESPONSE));
369+
expect(localStorage.length).toBe(1);
370+
done();
371+
});
372+
373+
it("logins, logouts successfully and no storage is filled (example: localStorage)", async done => {
374+
const fn = jest.fn();
375+
const identity = new OAuthIdentity({
376+
host: TEST_HOST,
377+
loginPath: LOGIN_PATH,
378+
logoutPath: LOGOUT_PATH
379+
});
380+
381+
mockLoginSuccess();
382+
identity.on("login", fn);
383+
await identity.login("", "");
384+
385+
expect(fn).toBeCalled();
386+
expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(null);
387+
expect(localStorage.length).toBe(0);
388+
389+
mockLogoutSuccess();
390+
identity.on("logout", fn);
391+
await identity.logout();
392+
393+
expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(null);
394+
expect(localStorage.length).toBe(0);
395+
expect(fn).toBeCalled();
396+
done();
397+
});
398+
399+
it("logout success with cleared saved identity data", async done => {
400+
const fn = jest.fn();
401+
const identity = new OAuthIdentity({
402+
host: TEST_HOST,
403+
loginPath: LOGIN_PATH,
404+
logoutPath: LOGOUT_PATH,
405+
storage: localStorage
406+
});
407+
408+
mockLoginSuccess();
409+
identity.on("login", fn);
410+
await identity.login("", "");
411+
expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(JSON.stringify(LOGIN_RESPONSE));
412+
expect(localStorage.length).toBe(1);
413+
expect(fn).toBeCalled();
414+
415+
mockLogoutSuccess();
416+
identity.on("logout", fn);
417+
await identity.logout();
418+
419+
expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(null);
420+
expect(localStorage.length).toBe(0);
421+
expect(fn).toBeCalled();
422+
done();
423+
});
424+
425+
it("logins successfully and localStorage filling with custom key", async done => {
426+
const fn = jest.fn();
427+
const identity = new OAuthIdentity({
428+
host: TEST_HOST,
429+
loginPath: LOGIN_PATH,
430+
logoutPath: LOGOUT_PATH,
431+
storage: localStorage,
432+
storageKey: "token"
433+
});
434+
435+
mockLoginSuccess();
436+
identity.on("login", fn);
437+
await identity.login("", "");
438+
439+
expect(fn).toBeCalled();
440+
expect(localStorage.getItem("token")).toBe(JSON.stringify(LOGIN_RESPONSE));
441+
expect(localStorage.length).toBe(1);
442+
done();
443+
});
444+
445+
it("local storage data fills OAuthIdentity mechanism", () => {
446+
localStorage.setItem(STORAGE_OAUTH_KEY, JSON.stringify(LOGIN_RESPONSE));
447+
448+
const identity = new OAuthIdentity({
449+
host: TEST_HOST,
450+
loginPath: LOGIN_PATH,
451+
logoutPath: LOGOUT_PATH,
452+
storage: localStorage
453+
});
454+
455+
expect(identity["oAuth"]).toEqual(LOGIN_RESPONSE);
456+
});

src/identities/oauth-identity.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,33 @@ import {
1010
HttpMethods,
1111
OAuthResponseDto
1212
} from "../contracts";
13+
import { STORAGE_OAUTH_KEY } from "../constants";
14+
import { isOAuthResponse } from "../helpers";
1315

1416
const IdentityEventEmitter: { new (): StrictEventEmitter<EventEmitter, IdentityMechanismEvents> } = EventEmitter;
1517
export class OAuthIdentity extends IdentityEventEmitter implements IdentityMechanism {
1618
constructor(protected readonly configuration: OAuthIdentityConfiguration) {
1719
super();
20+
21+
if (this.configuration.storage == null) {
22+
return;
23+
}
24+
25+
const storageKey = this.configuration.storageKey != null ? this.configuration.storageKey : STORAGE_OAUTH_KEY;
26+
const storageOAuthItem = this.configuration.storage.getItem(storageKey);
27+
28+
if (storageOAuthItem == null) {
29+
return;
30+
}
31+
32+
const parsedItem: unknown = JSON.parse(storageOAuthItem);
33+
34+
if (isOAuthResponse(parsedItem)) {
35+
this.oAuth = parsedItem;
36+
}
1837
}
1938

20-
private loginData: OAuthResponseDto | undefined;
39+
private oAuth: OAuthResponseDto | undefined;
2140
private renewalTimeoutId: number | undefined;
2241
/**
2342
* Value is set in seconds.
@@ -44,11 +63,11 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
4463
}
4564

4665
this.emit("login");
47-
this.setLoginData((await response.json()) as OAuthResponseDto);
66+
this.setOAuthData((await response.json()) as OAuthResponseDto);
4867
}
4968

5069
public async logout(): Promise<void> {
51-
if (this.loginData == null) {
70+
if (this.oAuth == null) {
5271
throw new Error("Identity: login data is not set yet.");
5372
}
5473

@@ -59,21 +78,27 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
5978
headers: { ...(this.configuration.headers || {}) },
6079
body: queryString.stringify({
6180
grant_type: "refresh_token",
62-
refresh_token: this.loginData.refresh_token
81+
refresh_token: this.oAuth.refresh_token
6382
})
6483
});
6584

6685
const responseStatus = `${response.status}`[0];
6786
if (responseStatus !== "2") {
6887
throw new Error("Failed to logout.");
6988
}
70-
this.loginData = undefined;
89+
this.oAuth = undefined;
7190
clearTimeout(this.renewalTimeoutId);
91+
7292
this.emit("logout");
93+
94+
if (this.configuration.storage == null) {
95+
return;
96+
}
97+
this.configuration.storage.clear();
7398
}
7499

75100
public async authenticateRequest(request: QueuedRequest): Promise<QueuedRequest> {
76-
if (this.loginData == null) {
101+
if (this.oAuth == null) {
77102
throw new Error("Identity: login data is not set yet.");
78103
}
79104

@@ -82,7 +107,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
82107
}
83108

84109
const authHeader: { [index: string]: string } = {
85-
Authorization: `${this.loginData.token_type} ${this.loginData.access_token}`
110+
Authorization: `${this.oAuth.token_type} ${this.oAuth.access_token}`
86111
};
87112

88113
request.headers = {
@@ -111,22 +136,27 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
111136
throw new Error("Failed renew token.");
112137
}
113138

114-
this.setLoginData((await response.json()) as OAuthResponseDto);
139+
this.setOAuthData((await response.json()) as OAuthResponseDto);
115140
}
116141

117-
private setLoginData(loginData: OAuthResponseDto): void {
118-
if (loginData.expires_in == null) {
142+
private setOAuthData(oAuthData: OAuthResponseDto): void {
143+
if (oAuthData.expires_in == null) {
119144
throw Error("Not supported without expiration time.");
120145
}
121146

122-
this.loginData = loginData;
147+
this.oAuth = oAuthData;
148+
149+
if (this.configuration.storage != null) {
150+
const storageKey = this.configuration.storageKey != null ? this.configuration.storageKey : STORAGE_OAUTH_KEY;
151+
this.configuration.storage.setItem(storageKey, JSON.stringify(oAuthData));
152+
}
123153

124154
// If response do not have `refresh_token` we are not using renewal mechanism.
125-
if (loginData.refresh_token == null) {
155+
if (oAuthData.refresh_token == null) {
126156
return;
127157
}
128158

129-
const refreshToken = loginData.refresh_token;
159+
const refreshToken = oAuthData.refresh_token;
130160

131161
// If response has `refresh_token` but we do not want to use renewal mechanism.
132162
if (this.configuration.tokenRenewalEnabled === false) {
@@ -138,7 +168,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
138168
this.renewalTimeoutId = undefined;
139169
}
140170

141-
const timeoutNumber = this.renewalTime(loginData.expires_in);
171+
const timeoutNumber = this.renewalTime(oAuthData.expires_in);
142172
this.renewalTimeoutId = window.setTimeout(() => this.renewToken(refreshToken), timeoutNumber);
143173
}
144174

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@
2222
}
2323
]
2424
},
25-
"exclude": ["**/__tests__", "node_modules", "dist", "configs"]
25+
"exclude": ["**/__tests__", "node_modules", "dist", "config"]
2626
}

0 commit comments

Comments
 (0)