Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sdk): handle relative URLs in #handleAuthorizationResponse() #5

Merged
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
35 changes: 35 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 2 additions & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
80 changes: 80 additions & 0 deletions packages/sdk/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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<string, any>) => {
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: '[email protected]',
};
});
});

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');
});

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');
});
});
});
10 changes: 9 additions & 1 deletion packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down