Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 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",
"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",
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/api-builder.test.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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",
Expand Down
9 changes: 5 additions & 4 deletions src/api-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
72 changes: 68 additions & 4 deletions src/identities/__tests__/oauth-identity.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import { OAuthIdentity } from "../oauth-identity";
import { LoginResponseDto, HttpMethods } from "../../contracts";
import { OAuthResponseDto, HttpMethods } from "../../contracts";
import fetchMock from "fetch-mock";
jest.useFakeTimers();

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",
refresh_token: "REFRESH_TOKEN",
// 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 {
Expand All @@ -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(
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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();
Expand All @@ -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();
});

Expand Down
24 changes: 18 additions & 6 deletions src/identities/oauth-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
QueuedRequest,
OAuthIdentityConfiguration,
HttpMethods,
LoginResponseDto
OAuthResponseDto
} from "../contracts";

const IdentityEventEmitter: { new (): StrictEventEmitter<EventEmitter, IdentityMechanismEvents> } = EventEmitter;
Expand All @@ -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.
Expand All @@ -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<void> {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 {
Expand Down