Skip to content

Commit

Permalink
Merge pull request #5 from Mermaid-Chart/fix/handle-relative-urls-in-…
Browse files Browse the repository at this point in the history
…handleAuthorizationResponse

fix(sdk): handle relative URLs in `#handleAuthorizationResponse()`
  • Loading branch information
sidharthv96 authored Nov 24, 2023
2 parents d6a15cc + da361b5 commit 838d435
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 3 deletions.
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

0 comments on commit 838d435

Please sign in to comment.