refactor(registratie): extract the read half of createDraftSync (RB-21)
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>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user