Skip to content

Commit

Permalink
feat(client): support node
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie committed Nov 10, 2021
1 parent 501c8a1 commit e463b28
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 82 deletions.
24 changes: 14 additions & 10 deletions packages/client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ const getResponseErrorMessage = async (response: Response): Promise<string> => {
}
};

export const requestWithFetch = async <T>(...args: Parameters<typeof fetch>): Promise<T> => {
const response = await fetch(...args);
if (!response.ok) {
throw new LogtoError({
message: await getResponseErrorMessage(response),
response,
});
}
export const createRequester = (fetchFunction: typeof fetch = fetch) => {
return async <T>(...args: Parameters<typeof fetch>): Promise<T> => {
const response = await fetchFunction(...args);
if (!response.ok) {
throw new LogtoError({
message: await getResponseErrorMessage(response),
response,
});
}

const data = (await response.json()) as T;
return data;
const data = (await response.json()) as T;
return data;
};
};

export type Requester = ReturnType<typeof createRequester>;
9 changes: 6 additions & 3 deletions packages/client/src/discover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as s from 'superstruct';

import { requestWithFetch } from './api';
import { createRequester, Requester } from './api';
import { LogtoError } from './errors';

const OIDCConfigurationSchema = s.type({
Expand All @@ -22,8 +22,11 @@ const appendSlashIfNeeded = (url: string): string => {
return url + '/';
};

export default async function discover(url: string): Promise<OIDCConfiguration> {
const response = await requestWithFetch<OIDCConfiguration>(
export default async function discover(
url: string,
requester: Requester = createRequester()
): Promise<OIDCConfiguration> {
const response = await requester<OIDCConfiguration>(
`${appendSlashIfNeeded(url)}oidc/.well-known/openid-configuration`
);

Expand Down
10 changes: 5 additions & 5 deletions packages/client/src/grant-token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ describe('grantTokenByRefreshToken', () => {
.post('/oidc/token')
.reply(200, successResponse);

const tokenSet = await grantTokenByRefreshToken(
'https://logto.dev/oidc/token',
'client_id',
'refresh_token'
);
const tokenSet = await grantTokenByRefreshToken({
endpoint: 'https://logto.dev/oidc/token',
clientId: 'client_id',
refreshToken: 'refresh_token',
});

expect(tokenSet).toMatchObject(successResponse);
});
Expand Down
30 changes: 16 additions & 14 deletions packages/client/src/grant-token.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as s from 'superstruct';

import { requestWithFetch } from './api';
import { createRequester, Requester } from './api';
import { LogtoError } from './errors';

const TokenSetParametersSchema = s.type({
Expand All @@ -12,29 +12,26 @@ const TokenSetParametersSchema = s.type({

export type TokenSetParameters = s.Infer<typeof TokenSetParametersSchema>;

type GrantTokenPayload = {
type GrantTokenByAuthorizationPayload = {
endpoint: string;
code: string;
redirectUri: string;
codeVerifier: string;
clientId: string;
};

export const grantTokenByAuthorizationCode = async ({
endpoint,
code,
redirectUri,
codeVerifier,
clientId,
}: GrantTokenPayload): Promise<TokenSetParameters> => {
export const grantTokenByAuthorizationCode = async (
{ endpoint, code, redirectUri, codeVerifier, clientId }: GrantTokenByAuthorizationPayload,
requester: Requester = createRequester()
): Promise<TokenSetParameters> => {
const parameters = new URLSearchParams();
parameters.append('grant_type', 'authorization_code');
parameters.append('code', code);
parameters.append('redirect_uri', redirectUri);
parameters.append('code_verifier', codeVerifier);
parameters.append('client_id', clientId);

const response = await requestWithFetch(endpoint, {
const response = await requester(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand All @@ -54,17 +51,22 @@ export const grantTokenByAuthorizationCode = async ({
}
};

type GrantTokenByRefreshTokenPayload = {
endpoint: string;
refreshToken: string;
clientId: string;
};

export const grantTokenByRefreshToken = async (
endpoint: string,
clientId: string,
refreshToken: string
{ endpoint, clientId, refreshToken }: GrantTokenByRefreshTokenPayload,
requester: Requester = createRequester()
): Promise<TokenSetParameters> => {
const parameters = new URLSearchParams();
parameters.append('grant_type', 'refresh_token');
parameters.append('client_id', clientId);
parameters.append('refresh_token', refreshToken);

const response = await requestWithFetch(endpoint, {
const response = await requester(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand Down
63 changes: 29 additions & 34 deletions packages/client/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Mock window.location
/* eslint-disable @silverhand/fp/no-delete */
/* eslint-disable @silverhand/fp/no-mutation */
import { KeyObject } from 'crypto';

import { SignJWT, generateKeyPair } from 'jose';
Expand Down Expand Up @@ -75,25 +72,12 @@ describe('LogtoClient', () => {
nock(BASE_URL).get('/oidc/.well-known/openid-configuration').reply(200, discoverResponse);
});

const locationBackup = window.location;

beforeAll(() => {
// Can not spy on `window.location` directly
// @ts-expect-error
delete window.location;
// @ts-expect-error
window.location = { assign: jest.fn() };
});

afterAll(() => {
window.location = locationBackup;
});

describe('createLogtoClient', () => {
test('create an instance', async () => {
const logto = await LogtoClient.create({
domain: DOMAIN,
clientId: CLIENT_ID,
storage: new MemoryStorage(),
});
expect(logto).toBeInstanceOf(LogtoClient);
});
Expand Down Expand Up @@ -143,13 +127,15 @@ describe('LogtoClient', () => {
});

describe('loginWithRedirect', () => {
test('window.location.assign should have been called', async () => {
test('onRedirect should have been called', async () => {
const onRedirect = jest.fn();
const logto = await LogtoClient.create({
domain: DOMAIN,
clientId: CLIENT_ID,
storage: new MemoryStorage(),
});
await logto.loginWithRedirect(REDIRECT_URI);
expect(window.location.assign).toHaveBeenCalled();
await logto.loginWithRedirect(REDIRECT_URI, onRedirect);
expect(onRedirect).toHaveBeenCalled();
});

test('session should be set', async () => {
Expand All @@ -159,7 +145,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
await logto.loginWithRedirect(REDIRECT_URI);
await logto.loginWithRedirect(REDIRECT_URI, jest.fn());
expect(storage.getItem('LOGTO_SESSION_MANAGER')).toHaveProperty('redirectUri', REDIRECT_URI);
expect(storage.getItem('LOGTO_SESSION_MANAGER')).toHaveProperty('codeVerifier');
});
Expand All @@ -173,7 +159,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
await logto.loginWithRedirect(REDIRECT_URI);
await logto.loginWithRedirect(REDIRECT_URI, jest.fn());
expect(logto.isLoginRedirect(REDIRECT_CALLBACK)).toBeTruthy();
});

Expand All @@ -194,7 +180,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
await logto.loginWithRedirect(REDIRECT_URI);
await logto.loginWithRedirect(REDIRECT_URI, jest.fn());
expect(logto.isLoginRedirect(REDIRECT_URI)).toBeFalsy();
});

Expand All @@ -205,7 +191,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
await logto.loginWithRedirect('http://example.com');
await logto.loginWithRedirect('http://example.com', jest.fn());
expect(logto.isLoginRedirect(REDIRECT_URI)).toBeFalsy();
});
});
Expand Down Expand Up @@ -248,7 +234,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
await logto.loginWithRedirect(REDIRECT_URI);
await logto.loginWithRedirect(REDIRECT_URI, jest.fn());
await logto.handleCallback(REDIRECT_CALLBACK);
expect(verifyIdToken).toHaveBeenCalled();
});
Expand All @@ -260,7 +246,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
await logto.loginWithRedirect(REDIRECT_URI);
await logto.loginWithRedirect(REDIRECT_URI, jest.fn());
await logto.handleCallback(REDIRECT_CALLBACK);
expect(storage.getItem('LOGTO_SESSION_MANAGER')).toBeUndefined();
});
Expand All @@ -272,7 +258,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
await logto.loginWithRedirect(REDIRECT_URI);
await logto.loginWithRedirect(REDIRECT_URI, jest.fn());
await logto.handleCallback(REDIRECT_CALLBACK);
expect(logto.isAuthenticated()).toBeTruthy();
});
Expand All @@ -298,6 +284,7 @@ describe('LogtoClient', () => {
const logto = await LogtoClient.create({
domain: DOMAIN,
clientId: CLIENT_ID,
storage: new MemoryStorage(),
});
await expect(logto.getAccessToken()).rejects.toThrow();
});
Expand All @@ -314,6 +301,7 @@ describe('LogtoClient', () => {
id_token: (await generateIdToken()).idToken,
expires_in: -1,
});
// eslint-disable-next-line @silverhand/fp/no-mutation
logto = await LogtoClient.create({
domain: DOMAIN,
clientId: CLIENT_ID,
Expand All @@ -332,13 +320,20 @@ describe('LogtoClient', () => {
});

describe('logout', () => {
test('window.location.assign should have been called', async () => {
test('onRedirect should have been called', async () => {
const onRedirect = jest.fn();
const storage = new MemoryStorage();
storage.setItem(LOGTO_TOKEN_SET_CACHE_KEY, {
...fakeTokenResponse,
id_token: (await generateIdToken()).idToken,
});
const logto = await LogtoClient.create({
domain: DOMAIN,
clientId: CLIENT_ID,
storage,
});
logto.logout(REDIRECT_URI);
expect(window.location.assign).toHaveBeenCalled();
logto.logout(REDIRECT_URI, onRedirect);
expect(onRedirect).toHaveBeenCalled();
});

test('login session should be cleared', async () => {
Expand All @@ -349,7 +344,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
logto.logout(REDIRECT_URI);
logto.logout(REDIRECT_URI, jest.fn());
expect(storage.removeItem).toBeCalledWith(SESSION_MANAGER_KEY);
});

Expand All @@ -365,7 +360,7 @@ describe('LogtoClient', () => {
clientId: CLIENT_ID,
storage,
});
logto.logout(REDIRECT_URI);
logto.logout(REDIRECT_URI, jest.fn());
expect(storage.removeItem).toBeCalled();
});
});
Expand Down Expand Up @@ -399,7 +394,7 @@ describe('LogtoClient', () => {
storage,
onAuthStateChange,
});
await logto.loginWithRedirect(REDIRECT_URI);
await logto.loginWithRedirect(REDIRECT_URI, jest.fn());
await logto.handleCallback(REDIRECT_CALLBACK);
expect(onAuthStateChange).toHaveBeenCalled();
});
Expand All @@ -413,7 +408,7 @@ describe('LogtoClient', () => {
storage,
onAuthStateChange,
});
logto.logout(REDIRECT_URI);
logto.logout(REDIRECT_URI, jest.fn());
expect(onAuthStateChange).toHaveBeenCalled();
});
});
Expand Down
Loading

0 comments on commit e463b28

Please sign in to comment.