diff --git a/package-lock.json b/package-lock.json index d9a5e59..fdb4bcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@reactway/api-builder", - "version": "1.0.0-alpha", + "version": "1.0.0-alpha.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 01ee4f8..f1d3b89 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@reactway/api-builder", - "version": "1.0.0-alpha", + "version": "1.0.0-alpha.2", "description": "An easy api client builder for applications with identity.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/__tests__/api-builder.test.ts b/src/__tests__/api-builder.test.ts index 06f2b4c..accb8cd 100644 --- a/src/__tests__/api-builder.test.ts +++ b/src/__tests__/api-builder.test.ts @@ -1,6 +1,6 @@ import { ApiBuilder } from "../api-builder"; import fetchMock from "fetch-mock"; -import { ApiRequestBinaryBody, LoginResponseDto, HttpMethods, ApiRequest } from "../contracts"; +import { ApiRequestBinaryBody, OAuthResponseDto, HttpMethods, ApiRequest } from "../contracts"; import { OAuthIdentity } from "../identities/oauth-identity"; jest.useFakeTimers(); @@ -16,7 +16,7 @@ const TEST_HOST = "https://example.com"; const LOGIN_PATH = "/api/login"; const LOGOUT_PATH = "/api/logout"; -const LOGIN_RESPONSE: LoginResponseDto = { +const LOGIN_RESPONSE: OAuthResponseDto = { scope: "offline_access", token_type: "Bearer", access_token: "ACCESS_TOKEN", diff --git a/src/api-builder.ts b/src/api-builder.ts index a6360fd..20544d0 100644 --- a/src/api-builder.ts +++ b/src/api-builder.ts @@ -57,21 +57,22 @@ export class ApiBuilder { const forceRequestIndex = this.requestsQueue.findIndex(x => x.isForced === true); const canMakeRequest = this.canMakeRequest(); - // FIXME: Refactor to be more readable (fail fast). + if (!canMakeRequest && forceRequestIndex === -1) { + return; + } + // If there are forced requests waiting in the queue. if (forceRequestIndex !== -1) { // Perform them first no matter whether we're allowed to make requests. // Take force request out of the queue. request = this.requestsQueue.splice(forceRequestIndex, 1)[0]; - } else if (canMakeRequest) { + } else { // Simply take FIFO request. const nextInQueue = this.requestsQueue.shift(); if (nextInQueue == null) { return; } request = nextInQueue; - } else { - return; } // Increment pending requests count. diff --git a/src/contracts.ts b/src/contracts.ts index efbc43e..9a138ca 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -87,11 +87,12 @@ export interface OAuthIdentityConfiguration { tokenRenewalEnabled?: boolean; } -export interface LoginResponseDto { - scope?: string; +export interface OAuthResponseDto { token_type: string; access_token: string; - expires_in: number; - refresh_token: string; + expires_in?: number; + scope?: string; + refresh_token?: string; + id_token?: string; } // #endregion diff --git a/src/identities/__tests__/oauth-identity.test.ts b/src/identities/__tests__/oauth-identity.test.ts index 41b09bf..279bb77 100644 --- a/src/identities/__tests__/oauth-identity.test.ts +++ b/src/identities/__tests__/oauth-identity.test.ts @@ -1,5 +1,5 @@ import { OAuthIdentity } from "../oauth-identity"; -import { LoginResponseDto, HttpMethods } from "../../contracts"; +import { OAuthResponseDto, HttpMethods } from "../../contracts"; import fetchMock from "fetch-mock"; jest.useFakeTimers(); @@ -7,7 +7,7 @@ const TEST_HOST = "https://example.com"; const LOGIN_PATH = "/api/login"; const LOGOUT_PATH = "/api/logout"; -const LOGIN_RESPONSE: LoginResponseDto = { +const LOGIN_RESPONSE: OAuthResponseDto = { scope: "offline_access", token_type: "Bearer", access_token: "ACCESS_TOKEN", @@ -15,6 +15,19 @@ const LOGIN_RESPONSE: LoginResponseDto = { // Seconds expires_in: 28800 }; +const LOGIN_RESPONSE_NO_EXPIRES_IN: OAuthResponseDto = { + scope: "offline_access", + token_type: "Bearer", + access_token: "ACCESS_TOKEN", + refresh_token: "REFRESH_TOKEN" +}; +const LOGIN_RESPONSE_NO_REFRESH_TOKEN: OAuthResponseDto = { + scope: "offline_access", + token_type: "Bearer", + access_token: "ACCESS_TOKEN", + // Seconds + expires_in: 28800 +}; // #region Mocked fetch results. function mockLoginSuccess(): void { @@ -26,6 +39,25 @@ function mockLoginSuccess(): void { }) ); } +function mockLoginSuccessNoExpiresIn(): void { + fetchMock.post( + `${TEST_HOST}${LOGIN_PATH}`, + Promise.resolve({ + status: 200, + body: JSON.stringify(LOGIN_RESPONSE_NO_EXPIRES_IN) + }) + ); +} + +function mockLoginSuccessNoRefreshToken(): void { + fetchMock.post( + `${TEST_HOST}${LOGIN_PATH}`, + Promise.resolve({ + status: 200, + body: JSON.stringify(LOGIN_RESPONSE_NO_REFRESH_TOKEN) + }) + ); +} function mockRenewFailed(): void { fetchMock.post( @@ -86,6 +118,38 @@ it("logins successfully", async done => { done(); }); +it("logins successfully with no expires_in property", async done => { + const identity = new OAuthIdentity({ + host: TEST_HOST, + loginPath: LOGIN_PATH, + logoutPath: LOGOUT_PATH + }); + + mockLoginSuccessNoExpiresIn(); + try { + await identity.login("", ""); + done.fail(); + } catch { + done(); + } +}); + +it("logins successfully with no refresh token", async done => { + const fn = jest.fn(); + const identity = new OAuthIdentity({ + host: TEST_HOST, + loginPath: LOGIN_PATH, + logoutPath: LOGOUT_PATH + }); + + mockLoginSuccessNoRefreshToken(); + identity.on("login", fn); + await identity.login("", ""); + + expect(fn).toBeCalled(); + done(); +}); + it("logins successfully with disabled renewal token", async done => { const fn = jest.fn(); const identity = new OAuthIdentity({ @@ -143,7 +207,7 @@ it("logins successfully with time renewal time less than expiration time", async host: TEST_HOST, loginPath: LOGIN_PATH, logoutPath: LOGOUT_PATH, - renewTokenTime: LOGIN_RESPONSE.expires_in + 100 + renewTokenTime: 28900 }); mockLoginSuccess(); @@ -167,7 +231,7 @@ it("logins successfully and new token", async done => { await identity.login("", ""); expect(fn).toBeCalled(); jest.runAllTimers(); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), LOGIN_RESPONSE.expires_in - 120); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 28680); done(); }); diff --git a/src/identities/oauth-identity.ts b/src/identities/oauth-identity.ts index 4b585f0..ccda2e9 100644 --- a/src/identities/oauth-identity.ts +++ b/src/identities/oauth-identity.ts @@ -8,7 +8,7 @@ import { QueuedRequest, OAuthIdentityConfiguration, HttpMethods, - LoginResponseDto + OAuthResponseDto } from "../contracts"; const IdentityEventEmitter: { new (): StrictEventEmitter } = EventEmitter; @@ -17,7 +17,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha super(); } - private loginData: LoginResponseDto | undefined; + private loginData: OAuthResponseDto | undefined; private renewalTimeoutId: number | undefined; /** * Value is set in seconds. @@ -44,7 +44,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha } this.emit("login"); - this.setLoginData((await response.json()) as LoginResponseDto); + this.setLoginData((await response.json()) as OAuthResponseDto); } public async logout(): Promise { @@ -111,12 +111,24 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha throw new Error("Failed renew token."); } - this.setLoginData((await response.json()) as LoginResponseDto); + this.setLoginData((await response.json()) as OAuthResponseDto); } - private setLoginData(loginData: LoginResponseDto): void { + private setLoginData(loginData: OAuthResponseDto): void { + if (loginData.expires_in == null) { + throw Error("Not supported without expiration time."); + } + this.loginData = loginData; + // If response do not have `refresh_token` we are not using renewal mechanism. + if (loginData.refresh_token == null) { + return; + } + + const refreshToken = loginData.refresh_token; + + // If response has `refresh_token` but we do not want to use renewal mechanism. if (this.configuration.tokenRenewalEnabled === false) { return; } @@ -127,7 +139,7 @@ export class OAuthIdentity extends IdentityEventEmitter implements IdentityMecha } const timeoutNumber = this.renewalTime(loginData.expires_in); - this.renewalTimeoutId = window.setTimeout(() => this.renewToken(loginData.refresh_token), timeoutNumber); + this.renewalTimeoutId = window.setTimeout(() => this.renewToken(refreshToken), timeoutNumber); } private renewalTime(time: number): number {