mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import type { AuthIdentity } from '../src/auth.js';
|
|
import {
|
|
PacksDomainError,
|
|
type PacksRepository,
|
|
} from '../src/domains/packs/repository.js';
|
|
import { registerPackRoutes } from '../src/domains/packs/routes.js';
|
|
|
|
const identity: AuthIdentity = {
|
|
id: '11111111-1111-4111-8111-111111111111',
|
|
email: 'alice@sauci.test',
|
|
name: 'Alice',
|
|
avatarUrl: null,
|
|
};
|
|
const packId = '22222222-2222-4222-8222-222222222222';
|
|
|
|
function repository(): PacksRepository {
|
|
return {
|
|
getCatalog: vi.fn(async () => ({ categories: [], packs: [] })),
|
|
getEnabledPacks: vi.fn(async () => ({ enabledPackIds: [packId] })),
|
|
setPackEnabled: vi.fn(async () => ({ enabledPackIds: [packId] })),
|
|
getPackProgress: vi.fn(async () => ({
|
|
progress: [{ packId, totalQuestions: 4, answeredQuestions: 2 }],
|
|
})),
|
|
close: vi.fn(async () => undefined),
|
|
};
|
|
}
|
|
|
|
function appWith(repository: PacksRepository) {
|
|
const app = new Hono<{ Variables: { identity: AuthIdentity } }>();
|
|
app.use('/v1/*', async (c, next) => {
|
|
c.set('identity', identity);
|
|
await next();
|
|
});
|
|
registerPackRoutes(app, repository);
|
|
return app;
|
|
}
|
|
|
|
describe('pack routes', () => {
|
|
it('derives catalog preferences and progress ownership from the authenticated identity', async () => {
|
|
const repo = repository();
|
|
const app = appWith(repo);
|
|
|
|
expect((await app.request('/v1/packs?showAllIntensities=true')).status).toBe(200);
|
|
expect(repo.getCatalog).toHaveBeenCalledWith(identity.id, true);
|
|
|
|
expect(await (await app.request('/v1/me/pack-progress')).json()).toEqual({
|
|
progress: [{ packId, totalQuestions: 4, answeredQuestions: 2 }],
|
|
});
|
|
expect(repo.getPackProgress).toHaveBeenCalledWith(identity.id);
|
|
});
|
|
|
|
it('updates the token owner couple and never accepts a user or couple ID from the client', async () => {
|
|
const repo = repository();
|
|
const app = appWith(repo);
|
|
const response = await app.request(`/v1/me/enabled-packs/${packId}`, {
|
|
method: 'PUT',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ enabled: true }),
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(repo.setPackEnabled).toHaveBeenCalledWith(identity.id, packId, true);
|
|
});
|
|
|
|
it('rejects malformed pack IDs and request bodies', async () => {
|
|
const repo = repository();
|
|
const app = appWith(repo);
|
|
|
|
expect((await app.request('/v1/me/enabled-packs/not-a-uuid', {
|
|
method: 'PUT', headers: { 'content-type': 'application/json' }, body: '{"enabled":true}',
|
|
})).status).toBe(400);
|
|
expect((await app.request(`/v1/me/enabled-packs/${packId}`, {
|
|
method: 'PUT', headers: { 'content-type': 'application/json' }, body: '{"enabled":"yes"}',
|
|
})).status).toBe(400);
|
|
expect(repo.setPackEnabled).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('maps couple and visibility failures without leaking another couple', async () => {
|
|
const repo = repository();
|
|
vi.mocked(repo.setPackEnabled).mockRejectedValueOnce(new PacksDomainError('no_couple'));
|
|
const app = appWith(repo);
|
|
const response = await app.request(`/v1/me/enabled-packs/${packId}`, {
|
|
method: 'PUT', headers: { 'content-type': 'application/json' }, body: '{"enabled":false}',
|
|
});
|
|
|
|
expect(response.status).toBe(409);
|
|
expect(await response.json()).toEqual({
|
|
error: { code: 'no_couple', message: 'Join a couple before changing packs' },
|
|
});
|
|
});
|
|
});
|