From cf2f6e14027a0718e4d07c7d0a57a6a8ae887970 Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 21 Nov 2023 13:10:29 +0000 Subject: [PATCH 1/3] test(sdk): test #handleAuthorizationResponse --- packages/sdk/package.json | 3 +- packages/sdk/src/index.test.ts | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/sdk/src/index.test.ts diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e089071..85a5528 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -14,7 +14,8 @@ "build:code:browser": "esbuild src/index.ts --bundle --minify --outfile=dist/bundle.iife.js", "build:code:node": "esbuild src/index.ts --bundle --platform=node --target=node18.18 --format=esm --packages=external --minify --outfile=dist/index.mjs", "build:types": "tsc -p ./tsconfig.json --emitDeclarationOnly", - "prepare": "pnpm run build" + "prepare": "pnpm run build", + "test": "vitest" }, "type": "module", "keywords": [], diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts new file mode 100644 index 0000000..c066454 --- /dev/null +++ b/packages/sdk/src/index.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { MermaidChart } from './index.js'; +import { AuthorizationData } from './types.js'; + +import { OAuth2Client } from '@badgateway/oauth2-client'; + +describe('MermaidChart', () => { + let client: MermaidChart; + beforeEach(() => { + vi.resetAllMocks(); + + vi.spyOn(OAuth2Client.prototype, 'request').mockImplementation( + async (endpoint: 'tokenEndpoint' | 'introspectionEndpoint', _body: Record) => { + switch (endpoint) { + case 'tokenEndpoint': + return { + access_token: 'test-example-access_token', + refresh_token: 'test-example-refresh_token', + token_type: 'Bearer', + expires_in: 3600, + }; + default: + throw new Error('mock unimplemented'); + } + }, + ); + + client = new MermaidChart({ + clientID: '00000000-0000-0000-0000-000000000dead', + baseURL: 'https://test.mermaidchart.invalid', + redirectURI: 'https://localhost.invalid', + }); + + vi.spyOn(client, 'getUser').mockImplementation(async () => { + return { + fullName: 'Test User', + emailAddress: 'test@invalid.invalid', + }; + }); + }); + + describe('#handleAuthorizationResponse', () => { + let state: AuthorizationData['state']; + beforeEach(async () => { + ({ state } = await client.getAuthorizationData({ state })); + }); + + it('should set token', async () => { + const code = 'hello-world'; + + await client.handleAuthorizationResponse( + `https://response.invalid?code=${code}&state=${state}`, + ); + await expect(client.getAccessToken()).resolves.toBe('test-example-access_token'); + }); + + it('should throw with invalid state', async () => { + await expect(() => + client.handleAuthorizationResponse( + 'https://response.invalid?code=hello-world&state=my-invalid-state', + ), + ).rejects.toThrowError('invalid_state'); + }); + }); +}); From 5f41a12b79110398b7a6aece045120042fbf3913 Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 21 Nov 2023 14:22:17 +0000 Subject: [PATCH 2/3] ci: add a GitHub Action to build and test the SDK --- .github/workflows/build.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..48594a9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build +on: + pull_request: + push: + +jobs: + test: + timeout-minutes: 15 + strategy: + matrix: + node: ['18.18.x'] + pkg: ['sdk'] + runs-on: + labels: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup Node.js ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + cache: pnpm + node-version: ${{ matrix.node }} + + - name: Install dependencies for ${{ matrix.pkg }} + run: | + pnpm install --frozen-lockfile --filter='...${{ matrix.pkg }}' + + - name: Test ${{ matrix.pkg }} + run: | + pnpm --filter='${{ matrix.pkg }}' test From da361b545c5323a41c648b839ff73e17732a6adc Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 21 Nov 2023 13:12:17 +0000 Subject: [PATCH 3/3] fix(sdk): fix handleAuthorizationResponse rel URL Support passing relative URLs for `#handleAuthorizationResponse()` on Node.JS. --- packages/sdk/CHANGELOG.md | 5 ++++- packages/sdk/src/index.test.ts | 15 +++++++++++++++ packages/sdk/src/index.ts | 10 +++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 2970bd0..a3ce293 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Compile an ESM version of this codebase for Node.JS v18. -## [0.1.1] - 2023-09-08 +### Fixed + +- `MermaidChart#handleAuthorizationResponse()` now supports relative URLs. +## [0.1.1] - 2023-09-08 - Browser-only build. diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index c066454..1a73688 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -61,5 +61,20 @@ describe('MermaidChart', () => { ), ).rejects.toThrowError('invalid_state'); }); + + it('should throw with nicer error if URL has no query params', async () => { + await expect(() => + client.handleAuthorizationResponse( + // missing the ? so it's not read as a query + 'code=hello-world&state=my-invalid-state', + ), + ).rejects.toThrowError(/no query parameters/); + }); + + it('should work in Node.JS with url fragment', async () => { + const code = 'hello-nodejs-world'; + await client.handleAuthorizationResponse(`?code=${code}&state=${state}`); + await expect(client.getAccessToken()).resolves.toBe('test-example-access_token'); + }); }); }); diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 31b88ab..8a4aab0 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -100,12 +100,20 @@ export class MermaidChart { }; } + /** + * Handle authorization response. + * + * @param urlString - URL, only the query string is required (e.g. `?code=xxxx&state=xxxxx`) + */ public async handleAuthorizationResponse(urlString: string) { - const url = new URL(urlString); + const url = new URL(urlString, 'https://dummy.invalid'); const state = url.searchParams.get('state') ?? undefined; const authorizationToken = url.searchParams.get('code'); if (!authorizationToken) { + if (url.searchParams.size === 0) { + throw new Error(`URL ${JSON.stringify(urlString)} has no query parameters.`); + } throw new RequiredParameterMissingError('token'); } if (!state) {