import { describe, it, expect, vi, afterEach } from 'vitest'; import { createReferralClient } from './client.js'; import type { ReferralDoc } from './types.js'; const baseConfig = { baseUrl: 'http://localhost:4003/api', productId: 'testapp', getAccessToken: () => 'test-token', }; function mockReferral(overrides?: Partial): ReferralDoc { return { id: 'ref-1', productId: 'testapp', referrerId: 'user-1', referrerEmail: 'alice@test.com', referredUserId: null, referredEmail: 'bob@test.com', status: 'pending', referrerRewardTokens: 1000, referredRewardTokens: 500, referrerRewarded: false, referredRewarded: false, createdAt: '2026-01-01T00:00:00Z', completedAt: null, ...overrides, }; } describe('createReferralClient', () => { afterEach(() => { vi.restoreAllMocks(); }); it('should create a referral', async () => { const ref = mockReferral(); vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(ref), }) ); const client = createReferralClient(baseConfig); const result = await client.createReferral({ referrerId: 'user-1', referrerEmail: 'alice@test.com', referredEmail: 'bob@test.com', }); expect(result).toEqual(ref); const fetchMock = globalThis.fetch as ReturnType; expect(fetchMock).toHaveBeenCalledWith( 'http://localhost:4003/api/referrals', expect.objectContaining({ method: 'POST' }) ); }); it('should list referrals by referrerId', async () => { const data = { referrals: [mockReferral()], count: 1 }; vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(data), }) ); const client = createReferralClient(baseConfig); const result = await client.listMyReferrals('user-1'); expect(result.count).toBe(1); expect(result.referrals).toHaveLength(1); const fetchMock = globalThis.fetch as ReturnType; expect(fetchMock).toHaveBeenCalledWith( 'http://localhost:4003/api/referrals/by-referrer/user-1', expect.any(Object) ); }); it('should get referral stats', async () => { const stats = { total: 10, completed: 5, rewarded: 3 }; vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(stats), }) ); const client = createReferralClient(baseConfig); const result = await client.getReferralStats(); expect(result).toEqual(stats); }); it('should update referral status via PUT', async () => { const updated = mockReferral({ status: 'signed_up' }); vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(updated), }) ); const client = createReferralClient(baseConfig); const result = await client.updateReferralStatus('ref-1', 'user-1', 'signed_up'); expect(result.status).toBe('signed_up'); const fetchMock = globalThis.fetch as ReturnType; expect(fetchMock).toHaveBeenCalledWith( 'http://localhost:4003/api/referrals/ref-1', expect.objectContaining({ method: 'PUT' }) ); }); it('should get referral by email', async () => { const ref = mockReferral(); vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(ref), }) ); const client = createReferralClient(baseConfig); const result = await client.getByEmail('bob@test.com'); expect(result).toEqual(ref); }); it('should return null for 404 on getByEmail', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: false, status: 404, }) ); const client = createReferralClient(baseConfig); const result = await client.getByEmail('unknown@test.com'); expect(result).toBeNull(); }); it('should send correct headers', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ total: 0, completed: 0, rewarded: 0 }), }) ); const client = createReferralClient(baseConfig); await client.getReferralStats(); const fetchMock = globalThis.fetch as ReturnType; const callHeaders = fetchMock.mock.calls[0][1].headers as Record; expect(callHeaders['x-product-id']).toBe('testapp'); expect(callHeaders['Authorization']).toBe('Bearer test-token'); expect(callHeaders['x-request-id']).toBeDefined(); }); it('should throw on non-ok response', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: false, status: 500, }) ); const client = createReferralClient(baseConfig); await expect(client.getReferralStats()).rejects.toThrow('getReferralStats failed: 500'); }); it('should build share link', () => { const client = createReferralClient(baseConfig); const link = client.buildShareLink('ABC123'); expect(link).toContain('refer/ABC123'); expect(link).toContain('product=testapp'); }); it('should build share message', () => { const client = createReferralClient(baseConfig); const msg = client.buildShareMessage('ABC123', 'NomGap'); expect(msg).toContain('NomGap'); expect(msg).toContain('refer/ABC123'); }); it('should calculate earned days', () => { const client = createReferralClient(baseConfig); expect(client.calculateEarnedDays(3)).toBe(21); expect(client.calculateEarnedDays(0)).toBe(0); expect(client.calculateEarnedDays(2, 14)).toBe(28); }); it('should use custom default reward tokens', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(mockReferral()), }) ); const client = createReferralClient({ ...baseConfig, defaultRewardTokens: { referrer: 2000, referred: 1000 }, }); await client.createReferral({ referrerId: 'user-1', referrerEmail: 'alice@test.com', referredEmail: 'bob@test.com', }); const fetchMock = globalThis.fetch as ReturnType; const body = JSON.parse(fetchMock.mock.calls[0][1].body as string); expect(body.referrerRewardTokens).toBe(2000); expect(body.referredRewardTokens).toBe(1000); }); });