createDraftSync mixed a read path (findConcept, load, the read half of resume) with its write path (ensureId, flush, submit, reset) in one 187-line function -- CQ-001's finding. Move findConcept and loadConcept into a new application/find-concept.ts as free functions that take the adapter, so they get a direct spec with no Angular TestBed. createDraftSync keeps the closure state (id, ensuring, resumeGate) and the whole write path unchanged -- this is a move, not a redesign. The resumeGate coupling that lets the write path wait for the read path stays exactly where it was. createDraftSync shrinks from 187 to 169 lines. draft-sync.spec.ts is unchanged -- it never called resume()/load() directly, and its 409 recovery test for submit() still exercises the extracted findConcept through ensureId's catch branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter';
|
|
import { findConcept, loadConcept } from './find-concept';
|
|
|
|
// Free functions taking the adapter as a parameter (no inject()) — a plain fake
|
|
// object is enough, no Angular TestBed needed.
|
|
function fakeAdapter(overrides: Partial<ApplicationsAdapter>): ApplicationsAdapter {
|
|
return overrides as ApplicationsAdapter;
|
|
}
|
|
|
|
describe('findConcept', () => {
|
|
it('returns the id of the existing Concept of the given type', async () => {
|
|
const adapter = fakeAdapter({
|
|
list: async () => [
|
|
{
|
|
id: 'a1',
|
|
type: 'registratie',
|
|
status: { tag: 'Concept', stepIndex: 0, stepCount: 3 },
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(findConcept(adapter, 'registratie')).resolves.toBe('a1');
|
|
});
|
|
|
|
it('returns undefined when the list has no application of the given type', async () => {
|
|
const adapter = fakeAdapter({ list: async () => [] });
|
|
|
|
await expect(findConcept(adapter, 'registratie')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined when the matching type is not a Concept', async () => {
|
|
const adapter = fakeAdapter({
|
|
list: async () => [
|
|
{
|
|
id: 'a1',
|
|
type: 'registratie',
|
|
status: { tag: 'Ingediend', referentie: 'R1' },
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(findConcept(adapter, 'registratie')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined when adapter.list() resolves with an unparsable shape', async () => {
|
|
const adapter = fakeAdapter({ list: async () => 'not-an-array' as unknown as [] });
|
|
|
|
await expect(findConcept(adapter, 'registratie')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined when adapter.list() rejects', async () => {
|
|
const adapter = fakeAdapter({
|
|
list: async () => {
|
|
throw new Error('network down');
|
|
},
|
|
});
|
|
|
|
await expect(findConcept(adapter, 'registratie')).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('loadConcept', () => {
|
|
it('reads the draft off a Concept', async () => {
|
|
const adapter = fakeAdapter({
|
|
detail: async () => ({
|
|
id: 'a1',
|
|
status: { tag: 'Concept', stepIndex: 1, stepCount: 3 },
|
|
draft: { step: 1 },
|
|
}),
|
|
});
|
|
|
|
await expect(loadConcept(adapter, 'a1')).resolves.toEqual({
|
|
tag: 'concept',
|
|
draft: { step: 1 },
|
|
});
|
|
});
|
|
|
|
it('reports a missing draft as null', async () => {
|
|
const adapter = fakeAdapter({
|
|
detail: async () => ({ id: 'a1', status: { tag: 'Concept', stepIndex: 0, stepCount: 3 } }),
|
|
});
|
|
|
|
await expect(loadConcept(adapter, 'a1')).resolves.toEqual({ tag: 'concept', draft: null });
|
|
});
|
|
|
|
it('reports not-concept when the id has moved past Concept (submitted)', async () => {
|
|
const adapter = fakeAdapter({
|
|
detail: async () => ({ id: 'a1', status: { tag: 'Ingediend', referentie: 'R1' } }),
|
|
});
|
|
|
|
await expect(loadConcept(adapter, 'a1')).resolves.toEqual({ tag: 'not-concept' });
|
|
});
|
|
|
|
it('reports not-concept when the id is unknown or deleted (detail rejects)', async () => {
|
|
const adapter = fakeAdapter({
|
|
detail: async () => {
|
|
throw new Error('404');
|
|
},
|
|
});
|
|
|
|
await expect(loadConcept(adapter, 'gone')).resolves.toEqual({ tag: 'not-concept' });
|
|
});
|
|
});
|