80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { createMissionTasksRepo } from './mission-tasks.js';
|
|
|
|
/**
|
|
* SHARED-CONTRACT §5.5 "mission_tasks.status write prohibition": this repo is
|
|
* the sole path that authors mission_tasks.status from caller input (storage
|
|
* tier migration is row transport and preserves stored values; the generic
|
|
* storage adapters have no mission_tasks caller), and it must never forward a
|
|
* caller-supplied status to the database on create or update. Callers keep
|
|
* working (the field is accepted and ignored), so these tests assert on what
|
|
* reaches the Drizzle chain, not on rejection.
|
|
*/
|
|
|
|
function makeInsertDb(returned: unknown[]) {
|
|
const values = vi.fn((_v: unknown) => ({ returning: vi.fn().mockResolvedValue(returned) }));
|
|
return { db: { insert: vi.fn(() => ({ values })) }, values };
|
|
}
|
|
|
|
function makeUpdateDb(returned: unknown[]) {
|
|
const set = vi.fn((_v: unknown) => ({
|
|
where: vi.fn(() => ({ returning: vi.fn().mockResolvedValue(returned) })),
|
|
}));
|
|
return { db: { update: vi.fn(() => ({ set })) }, set };
|
|
}
|
|
|
|
describe('createMissionTasksRepo — status write prohibition', () => {
|
|
it('create strips a caller-supplied status before insert', async () => {
|
|
const { db, values } = makeInsertDb([{ id: 'mt1', status: 'not-started' }]);
|
|
const repo = createMissionTasksRepo(db as never);
|
|
|
|
const result = await repo.create({
|
|
missionId: 'm1',
|
|
userId: 'u1',
|
|
status: 'done',
|
|
description: 'd',
|
|
} as never);
|
|
|
|
expect(values).toHaveBeenCalledTimes(1);
|
|
const inserted = values.mock.calls[0]![0] as Record<string, unknown>;
|
|
expect('status' in inserted).toBe(false);
|
|
expect(inserted.missionId).toBe('m1');
|
|
expect(inserted.description).toBe('d');
|
|
expect(result.id).toBe('mt1');
|
|
});
|
|
|
|
it('create without status still inserts (DB default applies)', async () => {
|
|
const { db, values } = makeInsertDb([{ id: 'mt2' }]);
|
|
const repo = createMissionTasksRepo(db as never);
|
|
|
|
await repo.create({ missionId: 'm1', userId: 'u1' } as never);
|
|
|
|
const inserted = values.mock.calls[0]![0] as Record<string, unknown>;
|
|
expect('status' in inserted).toBe(false);
|
|
});
|
|
|
|
it('update strips a caller-supplied status but keeps the other fields', async () => {
|
|
const { db, set } = makeUpdateDb([{ id: 'mt1', notes: 'n' }]);
|
|
const repo = createMissionTasksRepo(db as never);
|
|
|
|
const result = await repo.update('mt1', { status: 'done', notes: 'n' } as never);
|
|
|
|
expect(set).toHaveBeenCalledTimes(1);
|
|
const updated = set.mock.calls[0]![0] as Record<string, unknown>;
|
|
expect('status' in updated).toBe(false);
|
|
expect(updated.notes).toBe('n');
|
|
expect(updated.updatedAt).toBeInstanceOf(Date);
|
|
expect(result?.id).toBe('mt1');
|
|
});
|
|
|
|
it('update with only status degenerates to a timestamp-only update', async () => {
|
|
const { db, set } = makeUpdateDb([{ id: 'mt1' }]);
|
|
const repo = createMissionTasksRepo(db as never);
|
|
|
|
await repo.update('mt1', { status: 'blocked' } as never);
|
|
|
|
const updated = set.mock.calls[0]![0] as Record<string, unknown>;
|
|
expect(Object.keys(updated)).toEqual(['updatedAt']);
|
|
});
|
|
});
|