mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
113 lines
4.5 KiB
TypeScript
113 lines
4.5 KiB
TypeScript
import { sha256 } from '@noble/hashes/sha2';
|
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { nativeAuthApi } from '../api/nativeAuth';
|
|
import {
|
|
BrowserSignInCancelled,
|
|
createHandoffSecret,
|
|
handoffUrl,
|
|
rememberHandoffReturn,
|
|
requestHandoffCode,
|
|
signInThroughBrowser,
|
|
takeHandoffReturn,
|
|
} from './browserSignIn';
|
|
import { TrekShell } from './trekShell';
|
|
|
|
vi.mock('./trekShell', () => ({ APP_URL_SCHEME: 'com.liketrek.trek', TrekShell: { authenticate: vi.fn() } }));
|
|
vi.mock('../api/nativeAuth', () => ({ nativeAuthApi: { exchange: vi.fn(), handoff: vi.fn() } }));
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(TrekShell.authenticate).mockReset();
|
|
vi.mocked(nativeAuthApi.exchange).mockReset().mockResolvedValue();
|
|
});
|
|
|
|
describe('createHandoffSecret', () => {
|
|
it('pairs a fresh 64-character verifier with the hex sha256 the server checks against', () => {
|
|
const { verifier, challenge } = createHandoffSecret();
|
|
expect(verifier).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(challenge).toBe(bytesToHex(sha256(new TextEncoder().encode(verifier))));
|
|
expect(createHandoffSecret().verifier).not.toBe(verifier);
|
|
});
|
|
});
|
|
|
|
describe('requestHandoffCode', () => {
|
|
it('asks the server for the one-time code bound to the challenge', async () => {
|
|
vi.mocked(nativeAuthApi.handoff).mockResolvedValue({ code: 'c-1' });
|
|
await expect(requestHandoffCode('ab'.repeat(32))).resolves.toBe('c-1');
|
|
expect(nativeAuthApi.handoff).toHaveBeenCalledWith('ab'.repeat(32));
|
|
});
|
|
});
|
|
|
|
describe('handoffUrl', () => {
|
|
it('points at the hand-off page with the challenge and an optional start', () => {
|
|
expect(handoffUrl('https://trek.example.com', 'c')).toBe('https://trek.example.com/native-handoff?challenge=c');
|
|
expect(handoffUrl('https://trek.example.com', 'c', 'oidc')).toBe(
|
|
'https://trek.example.com/native-handoff?challenge=c&start=oidc'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('signInThroughBrowser', () => {
|
|
it('opens the hand-off page and redeems the returned code with the verifier', async () => {
|
|
vi.mocked(TrekShell.authenticate).mockResolvedValue({ url: 'com.liketrek.trek://auth?code=abc' });
|
|
|
|
await signInThroughBrowser('oidc');
|
|
|
|
const opened = new URL(vi.mocked(TrekShell.authenticate).mock.calls[0][0].url);
|
|
expect(opened.pathname).toBe('/native-handoff');
|
|
expect(opened.searchParams.get('start')).toBe('oidc');
|
|
expect(vi.mocked(TrekShell.authenticate).mock.calls[0][0].callbackScheme).toBe('com.liketrek.trek');
|
|
const { code, verifier } = vi.mocked(nativeAuthApi.exchange).mock.calls[0][0];
|
|
expect(code).toBe('abc');
|
|
expect(bytesToHex(sha256(new TextEncoder().encode(verifier)))).toBe(opened.searchParams.get('challenge'));
|
|
});
|
|
|
|
it('turns a closed browser into BrowserSignInCancelled', async () => {
|
|
vi.mocked(TrekShell.authenticate).mockRejectedValue(Object.assign(new Error('closed'), { code: 'CANCELLED' }));
|
|
await expect(signInThroughBrowser()).rejects.toBeInstanceOf(BrowserSignInCancelled);
|
|
expect(nativeAuthApi.exchange).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('passes other failures on', async () => {
|
|
vi.mocked(TrekShell.authenticate).mockRejectedValue(new Error('no browser'));
|
|
await expect(signInThroughBrowser()).rejects.toThrow('no browser');
|
|
});
|
|
|
|
it('refuses a return without a code', async () => {
|
|
vi.mocked(TrekShell.authenticate).mockResolvedValue({ url: 'com.liketrek.trek://auth' });
|
|
await expect(signInThroughBrowser()).rejects.toThrow('without a sign-in code');
|
|
expect(nativeAuthApi.exchange).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('the remembered handoff return', () => {
|
|
const path = `/native-handoff?challenge=${'a'.repeat(64)}`;
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('survives a sign-in that comes back in another tab, once', () => {
|
|
rememberHandoffReturn(path);
|
|
expect(takeHandoffReturn()).toBe(path);
|
|
expect(takeHandoffReturn()).toBeNull();
|
|
});
|
|
|
|
it('is dropped once it is older than a quarter of an hour', () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-11-08T12:00:00Z'));
|
|
rememberHandoffReturn(path);
|
|
vi.setSystemTime(new Date('2026-11-08T12:16:00Z'));
|
|
expect(takeHandoffReturn()).toBeNull();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('only ever leads back to the handoff page', () => {
|
|
rememberHandoffReturn('/admin');
|
|
expect(takeHandoffReturn()).toBeNull();
|
|
localStorage.setItem('trek_native_handoff_return', 'not json');
|
|
expect(takeHandoffReturn()).toBeNull();
|
|
});
|
|
});
|