Skip to content

Commit

Permalink
refactor(js,browser): reuse createRequester (#332)
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie committed Jul 14, 2022
1 parent 23066c9 commit cc33df4
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 21 deletions.
2 changes: 2 additions & 0 deletions packages/browser/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const crypto = require('crypto');

const { location } = require('jest-location-mock');
const fetch = require('node-fetch');
const { TextDecoder, TextEncoder } = require('text-encoder');
/* eslint-enable unicorn/prefer-module */

Expand All @@ -13,6 +14,7 @@ global.crypto = {
subtle: crypto.webcrypto.subtle,
};
global.location = location;
global.fetch = fetch;
global.TextDecoder = TextDecoder;
global.TextEncoder = TextEncoder;
/* eslint-enable @silverhand/fp/no-mutation */
1 change: 1 addition & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@silverhand/ts-config": "^0.15.0",
"@types/jest": "^27.4.0",
"eslint": "^8.9.0",
"node-fetch": "^2.6.7",
"jest": "^27.5.1",
"jest-location-mock": "^1.0.9",
"jest-matcher-specific-error": "^1.0.0",
Expand Down
16 changes: 2 additions & 14 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,23 @@
import BaseClient, { LogtoConfig, LogtoRequestErrorBody, LogtoRequestError } from '@logto/client';
import BaseClient, { createRequester, LogtoConfig } from '@logto/client';

import { BrowserStorage } from './storage';
import { generateCodeChallenge, generateCodeVerifier, generateState } from './utils/generators';

export type {
IdTokenClaims,
LogtoErrorCode,
LogtoRequestErrorBody,
LogtoConfig,
LogtoClientErrorCode,
} from '@logto/client';
export { LogtoError, OidcError, Prompt, LogtoRequestError, LogtoClientError } from '@logto/client';

const requester = async <T>(...args: Parameters<typeof fetch>): Promise<T> => {
const response = await fetch(...args);

if (!response.ok) {
// Expected request error from server
const { code, message } = await response.json<LogtoRequestErrorBody>();
throw new LogtoRequestError(code, message);
}

return response.json<T>();
};

const navigate = (url: string) => {
window.location.assign(url);
};

export default class LogtoClient extends BaseClient {
constructor(config: LogtoConfig) {
const requester = createRequester(fetch);
super(config, {
requester,
navigate,
Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ import { ClientAdapter } from './adapter';
import { LogtoClientError } from './errors';
import { buildAccessTokenKey, getDiscoveryEndpoint } from './utils';

export type { IdTokenClaims, LogtoErrorCode, LogtoRequestErrorBody } from '@logto/js';
export type { IdTokenClaims, LogtoErrorCode } from '@logto/js';
export { LogtoError, OidcError, Prompt, LogtoRequestError } from '@logto/js';
export * from './errors';
export type { Storage, StorageKey } from './adapter';
export { createRequester } from './utils';

export type LogtoConfig = {
endpoint: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { discoveryPath } from '@logto/js';

export * from './requester';

export const buildAccessTokenKey = (resource = '', scopes: string[] = []): string =>
`${scopes.slice().sort().join(' ')}@${resource}`;

Expand Down
74 changes: 74 additions & 0 deletions packages/client/src/utils/requester.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { LogtoRequestError } from '@logto/js';

import { createRequester } from './requester';

describe('createRequester', () => {
describe('successful response', () => {
test('should return data', async () => {
const data = { foo: 'bar' };
const fetchFunction = jest.fn().mockResolvedValue({
ok: true,
json: async () => data,
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).resolves.toEqual(data);
});
});

describe('request error', () => {
test('failing response json with code and message should throw LogtoRequestError with same code and message', async () => {
const code = 'some error code';
const message = 'some error message';
const fetchFunction = jest.fn().mockResolvedValue({
ok: false,
json: async () => ({ code, message }),
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).rejects.toMatchError(new LogtoRequestError(code, message));
});

test('failing response json with only code should throw StructError', async () => {
const fetchFunction = jest.fn().mockResolvedValue({
ok: false,
json: async () => {
throw new TypeError('without message');
},
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).rejects.toThrowError(TypeError);
});

test('failing response json with only message should throw StructError', async () => {
const fetchFunction = jest.fn().mockResolvedValue({
ok: false,
json: async () => {
throw new TypeError('without code');
},
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).rejects.toThrowError(TypeError);
});

test('failing response json without code and message should throw StructError', async () => {
const fetchFunction = jest.fn().mockResolvedValue({
ok: false,
json: async () => {
throw new TypeError('without code and message');
},
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).rejects.toThrowError(TypeError);
});

test('failing response with non-json text should throw TypeError', async () => {
const fetchFunction = jest.fn().mockResolvedValue({
ok: false,
json: async () => {
throw new TypeError('not json content');
},
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).rejects.toThrowError(TypeError);
});
});
});
15 changes: 15 additions & 0 deletions packages/client/src/utils/requester.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { LogtoRequestError, LogtoRequestErrorBody, Requester } from '@logto/js';

export const createRequester = (fetchFunction: typeof fetch): Requester => {
return async <T>(...args: Parameters<typeof fetch>): Promise<T> => {
const response = await fetchFunction(...args);

if (!response.ok) {
// Expected request error from server
const { code, message } = await response.json<LogtoRequestErrorBody>();
throw new LogtoRequestError(code, message);
}

return response.json<T>();
};
};
3 changes: 0 additions & 3 deletions packages/js/src/include.d/dom.d.ts

This file was deleted.

8 changes: 5 additions & 3 deletions pnpm-lock.yaml

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

0 comments on commit cc33df4

Please sign in to comment.