Files
Sauci/apps/api/test/couples.service.test.ts
T

75 lines
2.7 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),
cancelInvite: vi.fn(async () => undefined),
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);
});
it('uses the invite-only cancellation operation for a waiting creator', async () => {
const repo = repository();
const service = new CoupleService(repo);
await expect(service.cancelInvite('user-one')).resolves.toEqual({ success: true, couple_id: null });
expect(repo.cancelInvite).toHaveBeenCalledWith('user-one');
expect(repo.cancel).not.toHaveBeenCalled();
});
});