Skip to content

Commit

Permalink
Feature: added localstorage (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eimantas committed Apr 8, 2019
1 parent 29d23e7 commit 1272a56
Show file tree
Hide file tree
Showing 9 changed files with 5,540 additions and 5,354 deletions.
30 changes: 30 additions & 0 deletions config/jest/local-storage-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export class LocalStorageMock implements Storage {
private store: { [key: string]: string } = {};
[name: string]: unknown;

public key(index: number): string | null {
throw new Error("Method not implemented.");
}

public clear(): void {
this.store = {};
}

public getItem(key: string): string | null {
return this.store[key] || null;
}

public setItem(key: string, value: string): void {
this.store[key] = value;
}

public removeItem(key: string): void {
delete this.store[key];
}

public get length(): number {
return Object.keys(this.store).length;
}
}

Object.defineProperty(global, "localStorage", { value: new LocalStorageMock() });
10,680 changes: 5,343 additions & 5,337 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reactway/api-builder",
"version": "1.0.0-alpha.2",
"version": "1.0.0-alpha.3",
"description": "An easy api client builder for applications with identity.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -26,6 +26,8 @@
"Dovydas Navickas <dovydas@quatrodev.com> (https://github.com/DovydasNavickas)",
"Eimantas Dumšė <eimantas@quatrodev.com> (https://github.com/EimantasDumse)"
],
"repository": "github:reactway/api-builder",
"homepage": "https://github.com/reactway/api-builder",
"license": "MIT",
"devDependencies": {
"@types/fetch-mock": "^7.2.3",
Expand All @@ -35,6 +37,7 @@
"istanbul-azure-reporter": "^0.1.4",
"jest": "^24.5.0",
"jest-junit": "^6.3.0",
"jest-localstorage-mock": "^2.4.0",
"node-fetch": "^2.3.0",
"simplr-tslint": "^1.0.0-alpha.14",
"ts-jest": "^24.0.0",
Expand All @@ -54,6 +57,9 @@
"default",
"jest-junit"
],
"setupFiles": [
"./config/jest/local-storage-mock.ts"
],
"collectCoverage": true,
"testRegex": "/__tests__/.*\\.(test|spec).(ts|tsx)$",
"collectCoverageFrom": [
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const REQUEST_QUEUE_LIMIT = 5;
export const STORAGE_OAUTH_KEY = "OAuth";
2 changes: 2 additions & 0 deletions src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export interface OAuthIdentityConfiguration {
headers?: { [index: string]: string };
renewTokenTime?: number | ((time: number) => number);
tokenRenewalEnabled?: boolean;
storageKey?: string;
storage?: Storage;
}

export interface OAuthResponseDto {
Expand Down
7 changes: 6 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ApiRequestBinaryBody } from "./contracts";
import { ApiRequestBinaryBody, OAuthResponseDto } from "./contracts";

// tslint:disable-next-line:no-any
export function isBinaryBody(body: any): body is ApiRequestBinaryBody {
return body != null && (body as ApiRequestBinaryBody).isBinary === true;
}

// tslint:disable-next-line:no-any
export function isOAuthResponse(value: any): value is OAuthResponseDto {
return value != null && value.token_type != null && value.access_token != null;
}
106 changes: 106 additions & 0 deletions src/identities/__tests__/oauth-identity.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OAuthIdentity } from "../oauth-identity";
import { OAuthResponseDto, HttpMethods } from "../../contracts";
import fetchMock from "fetch-mock";
import { STORAGE_OAUTH_KEY } from "../../constants";
jest.useFakeTimers();

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

afterEach(() => {
fetchMock.restore();
localStorage.clear();
});

it("logins successfully", async done => {
Expand Down Expand Up @@ -348,3 +350,107 @@ it("no Login data is set yet", async done => {
done();
}
});
// LocalStorage
it("logins successfully and localStorage filled with data", async done => {
const fn = jest.fn();
const identity = new OAuthIdentity({
host: TEST_HOST,
loginPath: LOGIN_PATH,
logoutPath: LOGOUT_PATH,
storage: localStorage
});

mockLoginSuccess();
identity.on("login", fn);
await identity.login("", "");

expect(fn).toBeCalled();
expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(JSON.stringify(LOGIN_RESPONSE));
expect(localStorage.length).toBe(1);
done();
});

it("logins, logouts successfully and no storage is filled (example: localStorage)", async done => {
const fn = jest.fn();
const identity = new OAuthIdentity({
host: TEST_HOST,
loginPath: LOGIN_PATH,
logoutPath: LOGOUT_PATH
});

mockLoginSuccess();
identity.on("login", fn);
await identity.login("", "");

expect(fn).toBeCalled();
expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(null);
expect(localStorage.length).toBe(0);

mockLogoutSuccess();
identity.on("logout", fn);
await identity.logout();

expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(null);
expect(localStorage.length).toBe(0);
expect(fn).toBeCalled();
done();
});

it("logout success with cleared saved identity data", async done => {
const fn = jest.fn();
const identity = new OAuthIdentity({
host: TEST_HOST,
loginPath: LOGIN_PATH,
logoutPath: LOGOUT_PATH,
storage: localStorage
});

mockLoginSuccess();
identity.on("login", fn);
await identity.login("", "");
expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(JSON.stringify(LOGIN_RESPONSE));
expect(localStorage.length).toBe(1);
expect(fn).toBeCalled();

mockLogoutSuccess();
identity.on("logout", fn);
await identity.logout();

expect(localStorage.getItem(STORAGE_OAUTH_KEY)).toBe(null);
expect(localStorage.length).toBe(0);
expect(fn).toBeCalled();
done();
});

it("logins successfully and localStorage filling with custom key", async done => {
const fn = jest.fn();
const identity = new OAuthIdentity({
host: TEST_HOST,
loginPath: LOGIN_PATH,
logoutPath: LOGOUT_PATH,
storage: localStorage,
storageKey: "token"
});

mockLoginSuccess();
identity.on("login", fn);
await identity.login("", "");

expect(fn).toBeCalled();
expect(localStorage.getItem("token")).toBe(JSON.stringify(LOGIN_RESPONSE));
expect(localStorage.length).toBe(1);
done();
});

it("local storage data fills OAuthIdentity mechanism", () => {
localStorage.setItem(STORAGE_OAUTH_KEY, JSON.stringify(LOGIN_RESPONSE));

const identity = new OAuthIdentity({
host: TEST_HOST,
loginPath: LOGIN_PATH,
logoutPath: LOGOUT_PATH,
storage: localStorage
});

expect(identity["oAuth"]).toEqual(LOGIN_RESPONSE);
});
58 changes: 44 additions & 14 deletions src/identities/oauth-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,33 @@ import {
HttpMethods,
OAuthResponseDto
} from "../contracts";
import { STORAGE_OAUTH_KEY } from "../constants";
import { isOAuthResponse } from "../helpers";

const IdentityEventEmitter: { new (): StrictEventEmitter<EventEmitter, IdentityMechanismEvents> } = EventEmitter;
export class OAuthIdentity extends IdentityEventEmitter implements IdentityMechanism {
constructor(protected readonly configuration: OAuthIdentityConfiguration) {
super();

if (this.configuration.storage == null) {
return;
}

const storageKey = this.configuration.storageKey != null ? this.configuration.storageKey : STORAGE_OAUTH_KEY;
const storageOAuthItem = this.configuration.storage.getItem(storageKey);

if (storageOAuthItem == null) {
return;
}

const parsedItem: unknown = JSON.parse(storageOAuthItem);

if (isOAuthResponse(parsedItem)) {
this.oAuth = parsedItem;
}
}

private loginData: OAuthResponseDto | undefined;
private oAuth: OAuthResponseDto | undefined;
private renewalTimeoutId: number | undefined;
/**
* Value is set in seconds.
Expand All @@ -44,11 +63,11 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
}

this.emit("login");
this.setLoginData((await response.json()) as OAuthResponseDto);
this.setOAuthData((await response.json()) as OAuthResponseDto);
}

public async logout(): Promise<void> {
if (this.loginData == null) {
if (this.oAuth == null) {
throw new Error("Identity: login data is not set yet.");
}

Expand All @@ -59,21 +78,27 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
headers: { ...(this.configuration.headers || {}) },
body: queryString.stringify({
grant_type: "refresh_token",
refresh_token: this.loginData.refresh_token
refresh_token: this.oAuth.refresh_token
})
});

const responseStatus = `${response.status}`[0];
if (responseStatus !== "2") {
throw new Error("Failed to logout.");
}
this.loginData = undefined;
this.oAuth = undefined;
clearTimeout(this.renewalTimeoutId);

this.emit("logout");

if (this.configuration.storage == null) {
return;
}
this.configuration.storage.clear();
}

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

Expand All @@ -82,7 +107,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha
}

const authHeader: { [index: string]: string } = {
Authorization: `${this.loginData.token_type} ${this.loginData.access_token}`
Authorization: `${this.oAuth.token_type} ${this.oAuth.access_token}`
};

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

this.setLoginData((await response.json()) as OAuthResponseDto);
this.setOAuthData((await response.json()) as OAuthResponseDto);
}

private setLoginData(loginData: OAuthResponseDto): void {
if (loginData.expires_in == null) {
private setOAuthData(oAuthData: OAuthResponseDto): void {
if (oAuthData.expires_in == null) {
throw Error("Not supported without expiration time.");
}

this.loginData = loginData;
this.oAuth = oAuthData;

if (this.configuration.storage != null) {
const storageKey = this.configuration.storageKey != null ? this.configuration.storageKey : STORAGE_OAUTH_KEY;
this.configuration.storage.setItem(storageKey, JSON.stringify(oAuthData));
}

// If response do not have `refresh_token` we are not using renewal mechanism.
if (loginData.refresh_token == null) {
if (oAuthData.refresh_token == null) {
return;
}

const refreshToken = loginData.refresh_token;
const refreshToken = oAuthData.refresh_token;

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

const timeoutNumber = this.renewalTime(loginData.expires_in);
const timeoutNumber = this.renewalTime(oAuthData.expires_in);
this.renewalTimeoutId = window.setTimeout(() => this.renewToken(refreshToken), timeoutNumber);
}

Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@
}
]
},
"exclude": ["**/__tests__", "node_modules", "dist", "configs"]
"exclude": ["**/__tests__", "node_modules", "dist", "config"]
}

0 comments on commit 1272a56

Please sign in to comment.