Files
Sauci/apps/api/test/couples.service.test.ts
T
jjbruntonandClaude Fable 5 09255da7db feat(pairing): solo sealed answers that unlock on pairing
Unpaired users could only reach a waiting-room dead end and could not answer
questions until a partner joined. Responses.couple_id is now nullable so a
solo user answers immediately and banks "sealed answers" (couple_id IS NULL).
Pairing claims both members' sealed answers and computes matches for every
question both have now answered. Invite copy and the unpaired push nudge
escalate with the sealed answer count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 19:34:42 +01:00

65 lines
2.3 KiB
TypeScript

import type { Couple } from '@sauci/shared';
import { describe, expect, it, vi } from 'vitest';
import type { CoupleRepository } from '../src/domains/couples/repository.js';
import { CoupleService, generateInviteCode } from '../src/domains/couples/service.js';
import { CoupleError } from '../src/domains/couples/types.js';
const couple: Couple = {
id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
invite_code: 'ABCD2345',
created_at: '2026-08-27T00:00:00.000Z',
};
function repository(overrides: Partial<CoupleRepository> = {}): CoupleRepository {
return {
getState: vi.fn(async () => ({ couple: null, partner: null, sealed_count: 0 })),
create: vi.fn(async () => couple),
join: vi.fn(async () => couple),
cancel: vi.fn(async () => undefined),
close: vi.fn(async () => undefined),
...overrides,
};
}
describe('CoupleService', () => {
it('generates an eight-character unambiguous uppercase invite code', () => {
expect(generateInviteCode(Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]))).toBe('ABCDEFGH');
});
it('canonicalizes join codes and never accepts a caller-supplied owner', async () => {
const repo = repository();
const service = new CoupleService(repo);
await expect(service.join('user-one', ' abcd2345 ')).resolves.toEqual({
success: true,
couple_id: couple.id,
});
expect(repo.join).toHaveBeenCalledWith('user-one', 'ABCD2345');
});
it('rejects malformed invite codes before repository access', async () => {
const repo = repository();
const service = new CoupleService(repo);
await expect(service.join('user-one', 'short')).rejects.toMatchObject({
code: 'invalid_invite_code',
status: 400,
});
expect(repo.join).not.toHaveBeenCalled();
});
it('retries an invite-code collision', async () => {
const create = vi.fn()
.mockRejectedValueOnce(new CoupleError('invite_code_collision', 'collision', 409))
.mockResolvedValueOnce(couple);
const service = new CoupleService(repository({ create }));
await expect(service.create('user-one')).resolves.toMatchObject({
success: true,
couple_id: couple.id,
});
expect(create).toHaveBeenCalledTimes(2);
expect(create.mock.calls.every(([userId]) => userId === 'user-one')).toBe(true);
});
});