diff --git a/qr-device-login/client/test/client.test.ts b/qr-device-login/client/test/client.test.ts new file mode 100644 index 0000000..0ed594a --- /dev/null +++ b/qr-device-login/client/test/client.test.ts @@ -0,0 +1,92 @@ +/** + * Client SDK unit tests. + * + * Tests input validation and error mapping. We don't stand up a real HTTP + * server — the purpose is to verify the SDK validates inputs client-side + * before ever making a network call, and that error shapes are correctly + * typed. + */ + +import { describe, expect, it } from 'vitest'; +import { + QrLoginError, + QrLoginSessionExpired, + QrLoginAlreadyConsumed, + QrLoginWrongCode, + QrLoginSessionNotFound, + QrLoginUnauthorized, +} from '../src/errors'; +import { errorFromCode } from '../src/errors'; + +describe('QrLoginError hierarchy', () => { + it('maps session_expired to QrLoginSessionExpired', () => { + const err = errorFromCode('session_expired', 'test', 410); + expect(err).toBeInstanceOf(QrLoginSessionExpired); + expect(err).toBeInstanceOf(QrLoginError); + expect(err.code).toBe('session_expired'); + expect(err.httpStatus).toBe(410); + }); + + it('maps session_already_consumed to QrLoginAlreadyConsumed', () => { + const err = errorFromCode('session_already_consumed', 'test', 410); + expect(err).toBeInstanceOf(QrLoginAlreadyConsumed); + expect(err.code).toBe('session_already_consumed'); + }); + + it('maps wrong_code to QrLoginWrongCode', () => { + const err = errorFromCode('wrong_code', 'test', 403); + expect(err).toBeInstanceOf(QrLoginWrongCode); + expect(err.code).toBe('wrong_code'); + }); + + it('maps session_not_found to QrLoginSessionNotFound', () => { + const err = errorFromCode('session_not_found', 'test', 404); + expect(err).toBeInstanceOf(QrLoginSessionNotFound); + expect(err.code).toBe('session_not_found'); + }); + + it('maps unauthorized to QrLoginUnauthorized', () => { + const err = errorFromCode('unauthorized', 'test', 401); + expect(err).toBeInstanceOf(QrLoginUnauthorized); + expect(err.code).toBe('unauthorized'); + }); + + it('maps unknown codes to base QrLoginError', () => { + const err = errorFromCode('rate_limited', 'test', 429); + expect(err).toBeInstanceOf(QrLoginError); + expect(err.code).toBe('rate_limited'); + expect(err.httpStatus).toBe(429); + }); +}); + +describe('createQrLoginClient input validation', async () => { + // Dynamically import so we can test without a real connection. + // The client constructor doesn't connect — validation happens at call time. + const { createQrLoginClient } = await import('../src/client'); + + // Provide dummy cert values — the client won't actually connect. + const client = createQrLoginClient({ + baseUrl: 'https://qr-auth.test:8443', + cert: 'fake-cert', + key: 'fake-key', + ca: 'fake-ca', + }); + + it('rejects empty callbackUrl in createSession', async () => { + await expect( + client.createSession({ callbackUrl: '' }), + ).rejects.toThrow('callbackUrl is required'); + }); + + it('rejects empty id in pollSession', async () => { + await expect(client.pollSession('')).rejects.toThrow('id is required'); + }); + + it('rejects empty id in exchangeCode', async () => { + await expect(client.exchangeCode('', 'code')).rejects.toThrow('id is required'); + }); + + it('rejects empty code in exchangeCode', async () => { + await expect(client.exchangeCode('some-id', '')).rejects.toThrow('code is required'); + }); +});