import { describe, expect, it } from 'bun:test'; import { exchangeCloudAppToken, CloudAppTokenExchangeError, type CloudAppFetch } from '../cloud-app-token.service'; const okResponse = (body: unknown): Response => new Response(JSON.stringify(body), { status: 200, headers: { 'Content-Type': 'application/json' }, }); describe('exchangeCloudAppToken', () => { it('exchanges credentials for a token on 200 and parses expiresAt', async () => { const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString(); const calls: Array<{ url: string; init?: { method?: string; body?: string } }> = []; const fakeFetch: CloudAppFetch = (input, init) => { calls.push({ url: input, init }); return Promise.resolve(okResponse({ token: 'jwt-abc', expiresAt })); }; const result = await exchangeCloudAppToken({ appRegistrationId: 'my-app', appSecret: 'shh', coreApiUrl: 'http://localhost:14080', fetch: fakeFetch, }); expect(result.token).toBe('jwt-abc'); expect(result.expiresAt).toBe(expiresAt); expect(result.expiresAtMs).toBe(Date.parse(expiresAt)); expect(calls).toHaveLength(1); expect(calls[0]?.url).toBe('http://localhost:14080/api/2026-03/cloud-apps/my-app/token'); expect(calls[0]?.init?.method).toBe('POST'); expect(calls[0]?.init?.body).toBe(JSON.stringify({ appSecret: 'shh' })); }); it('throws CloudAppTokenExchangeError with status=401 on invalid credentials', async () => { const fakeFetch: CloudAppFetch = () => Promise.resolve(new Response('unauthorized', { status: 401 })); let caught: unknown; try { await exchangeCloudAppToken({ appRegistrationId: 'a', appSecret: 'wrong', coreApiUrl: 'http://core', fetch: fakeFetch, }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(CloudAppTokenExchangeError); expect((caught as CloudAppTokenExchangeError).status).toBe(401); expect((caught as Error).message).toContain('invalid credentials'); }); it('surfaces 5xx responses with body text in the error message', async () => { const fakeFetch: CloudAppFetch = () => Promise.resolve(new Response('boom', { status: 503 })); let caught: unknown; try { await exchangeCloudAppToken({ appRegistrationId: 'a', appSecret: 'secret', coreApiUrl: 'http://core', fetch: fakeFetch, }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(CloudAppTokenExchangeError); expect((caught as CloudAppTokenExchangeError).status).toBe(503); expect((caught as Error).message).toContain('HTTP 503'); expect((caught as Error).message).toContain('boom'); }); it('throws when network fetch fails', async () => { const fakeFetch: CloudAppFetch = () => Promise.reject(new Error('econnrefused')); let caught: unknown; try { await exchangeCloudAppToken({ appRegistrationId: 'a', appSecret: 'secret', coreApiUrl: 'http://core', fetch: fakeFetch, }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(CloudAppTokenExchangeError); expect((caught as Error).message).toContain('econnrefused'); }); it('throws when response body is missing required fields', async () => { const fakeFetch: CloudAppFetch = () => Promise.resolve(okResponse({ token: 'only-token' })); let caught: unknown; try { await exchangeCloudAppToken({ appRegistrationId: 'a', appSecret: 'secret', coreApiUrl: 'http://core', fetch: fakeFetch, }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(CloudAppTokenExchangeError); expect((caught as Error).message).toContain('unexpected payload shape'); }); it('throws when expiresAt is not a parseable date', async () => { const fakeFetch: CloudAppFetch = () => Promise.resolve(okResponse({ token: 'jwt', expiresAt: 'not-a-date' })); let caught: unknown; try { await exchangeCloudAppToken({ appRegistrationId: 'a', appSecret: 'secret', coreApiUrl: 'http://core', fetch: fakeFetch, }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(CloudAppTokenExchangeError); expect((caught as Error).message).toContain('unparseable expiresAt'); }); it('throws when required params are missing', async () => { const fakeFetch: CloudAppFetch = () => Promise.resolve(okResponse({})); let caught: unknown; try { await exchangeCloudAppToken({ appRegistrationId: '', appSecret: 'x', coreApiUrl: 'http://core', fetch: fakeFetch, }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(CloudAppTokenExchangeError); expect((caught as Error).message).toContain('appRegistrationId required'); }); }); // Re-export type helper used by other test files export type { CloudAppFetch };