// The one thing about the OAuth client that can be tested without a browser: // that both halves of the flow name the same redirect URI, and that it is this // build's own id rather than whichever id the client metadata happens to list // first. Getting that wrong fails only at the token exchange, only for the ids // that are not first, and only in a real sign-in — so it is worth pinning here. import { beforeEach, describe, expect, it, vi } from 'vitest' interface RedirectOption { redirect_uri?: string } const authorize = vi.fn( async (_input: string, _options?: RedirectOption) => new URL('https://pds.example.com/authorize?x=1'), ) const callback = vi.fn(async (_params: URLSearchParams, _options?: RedirectOption) => ({ session: { sub: 'did:plc:abc123' }, })) vi.mock('@atproto/oauth-client-browser', () => ({ BrowserOAuthClient: class { authorize = authorize callback = callback }, })) // buildSessionInfo resolves the handle and avatar; both are cosmetic and this // test is about the arguments, not the profile. vi.mock('./atproto', () => ({ resolveDid: vi.fn(async () => { throw new Error('no network in tests') }), profileAvatarUrl: vi.fn(), })) const EXTENSION_ID = 'degljbilkggdpbobomfbgnellecgbkjj' const REDIRECT = `https://${EXTENSION_ID}.chromiumapp.org/oauth2` vi.stubGlobal('chrome', { runtime: { id: EXTENSION_ID } }) const { completeAuthorization, startAuthorization } = await import('./oauth') // The store id, listed first in oauth/client-metadata.json — what the client // falls back to when a call does not say otherwise. const metadata = (await import('../../oauth/client-metadata.json')).default beforeEach(() => { authorize.mockClear() callback.mockClear() }) describe('the interactive sign-in flow', () => { it('authorizes with the redirect uri of the id this build runs under', async () => { await startAuthorization('reader.example.com') expect(authorize).toHaveBeenCalledWith('reader.example.com', { redirect_uri: REDIRECT }) }) it('exchanges the code with the same redirect uri it authorized with', async () => { await completeAuthorization(`${REDIRECT}?code=abc&state=xyz`) const [params, options] = callback.mock.calls[0] ?? [] expect(params?.get('code')).toBe('abc') expect(options).toEqual({ redirect_uri: REDIRECT }) }) it('does not leave the exchange to fall back to the first listed id', async () => { // The regression: with no options the client sends redirect_uris[0], so an // unpacked build authorizes as itself and exchanges as the store. expect(metadata.redirect_uris[0]).not.toBe(REDIRECT) await completeAuthorization(`${REDIRECT}?code=abc`) const sent = callback.mock.calls[0]?.[1]?.redirect_uri expect(sent).toBe(REDIRECT) expect(sent).not.toBe(metadata.redirect_uris[0]) }) })