mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
75 lines
2.7 KiB
TypeScript
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();
|
|
});
|
|
});
|