@martinffx/atelier 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +27 -0
- package/CHANGELOG.md +33 -0
- package/README.md +51 -59
- package/agents/architect.md +1 -1
- package/agents/oracle.md +1 -1
- package/dist/atelier.js +14171 -12949
- package/package.json +6 -1
- package/skills/code-commit/SKILL.md +77 -77
- package/skills/code-handoff/SKILL.md +1 -1
- package/skills/code-pull-request/SKILL.md +36 -0
- package/skills/code-pull-request/references/common.md +76 -0
- package/skills/code-pull-request/references/create-pr.md +122 -0
- package/skills/code-pull-request/references/default-template.md +25 -0
- package/skills/code-pull-request/references/leave-comment.md +53 -0
- package/skills/code-pull-request/references/merge-pr.md +94 -0
- package/skills/code-pull-request/references/read-comments.md +61 -0
- package/skills/code-pull-request/references/respond-to-comment.md +66 -0
- package/skills/code-review/SKILL.md +21 -12
- package/skills/code-review/references/reviewers.md +142 -41
- package/skills/code-review/references/rq.md +48 -32
- package/skills/oracle-debug/SKILL.md +218 -0
- package/skills/{code-debug → oracle-debug}/references/bug-patterns.md +3 -3
- package/skills/oracle-domain-modelling/SKILL.md +84 -0
- package/skills/oracle-grill-me/SKILL.md +20 -0
- package/skills/spec-brainstorm/SKILL.md +91 -71
- package/skills/spec-finish/SKILL.md +20 -14
- package/skills/spec-implement/SKILL.md +3 -2
- package/skills/spec-orchestrator/SKILL.md +4 -15
- package/skills/spec-plan/SKILL.md +1 -1
- package/agents/scout.md +0 -40
- package/skills/code-commit/references/pr-workflow.md +0 -145
- package/skills/code-debug/SKILL.md +0 -199
- package/skills/code-docs/SKILL.md +0 -171
- package/skills/code-docs/references/api-docs.md +0 -116
- package/skills/code-docs/references/changelog.md +0 -64
- package/skills/code-docs/references/readme-structure.md +0 -94
- package/skills/oracle-architect/SKILL.md +0 -327
- package/skills/oracle-architect/references/api-design.md +0 -345
- package/skills/oracle-architect/references/data-modeling.md +0 -318
- package/skills/oracle-architect/references/ddd-patterns.md +0 -348
- package/skills/oracle-architect/references/interface-design.md +0 -203
- package/skills/oracle-challenge/SKILL.md +0 -126
- package/skills/oracle-doubt/SKILL.md +0 -252
- package/skills/oracle-doubt/references/adversarial-prompt.md +0 -47
- package/skills/oracle-grillme/SKILL.md +0 -160
- package/skills/oracle-security/SKILL.md +0 -225
- package/skills/oracle-security/references/security-patterns.md +0 -236
- package/skills/oracle-security/references/threat-modeling-guide.md +0 -137
- package/skills/oracle-testing/SKILL.md +0 -237
- package/skills/oracle-testing/references/anti-patterns.md +0 -618
- package/skills/oracle-testing/references/boundaries.md +0 -579
- package/skills/oracle-testing/references/core-testing.md +0 -691
- package/skills/oracle-testing/references/edge-testing.md +0 -794
- package/skills/oracle-testing/references/stub-driven-tdd.md +0 -322
- package/skills/oracle-thinkdeep/SKILL.md +0 -164
- /package/skills/{code-debug → oracle-debug}/references/language-tools.md +0 -0
- /package/skills/{code-debug → oracle-debug}/references/techniques.md +0 -0
- /package/skills/{oracle-grillme → oracle-domain-modelling}/references/ADR-FORMAT.md +0 -0
- /package/skills/{oracle-grillme → oracle-domain-modelling}/references/CONTEXT-FORMAT.md +0 -0
|
@@ -1,579 +0,0 @@
|
|
|
1
|
-
# Layer Boundary Testing Patterns
|
|
2
|
-
|
|
3
|
-
Detailed boundary testing patterns for each layer in the architecture.
|
|
4
|
-
|
|
5
|
-
## Core Principle
|
|
6
|
-
|
|
7
|
-
Test the contract between layers, not the implementation within layers.
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
Test here ──────▼──────────────────▼────── Test here
|
|
11
|
-
Effectful Edge │ Functional Core
|
|
12
|
-
(stub) │ (unit test)
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Entity Boundary (Functional Core)
|
|
16
|
-
|
|
17
|
-
Pure functions with no dependencies.
|
|
18
|
-
|
|
19
|
-
### What to Test at Entity Boundary
|
|
20
|
-
|
|
21
|
-
- Validation rules
|
|
22
|
-
- Business rules
|
|
23
|
-
- Data transformations (fromRequest, toRecord, toResponse)
|
|
24
|
-
- Invariants and state management
|
|
25
|
-
- Edge cases and error conditions
|
|
26
|
-
|
|
27
|
-
### Entity Test Examples
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
describe('Order entity', () => {
|
|
31
|
-
describe('validation', () => {
|
|
32
|
-
it('rejects empty items', () => {
|
|
33
|
-
const order = new Order('1', 'C1', [], 'pending', 0);
|
|
34
|
-
const result = order.validate();
|
|
35
|
-
expect(result.ok).toBe(false);
|
|
36
|
-
expect(result.error).toContain('at least one item');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('rejects negative total', () => {
|
|
40
|
-
const order = new Order('1', 'C1', [], 'pending', -10);
|
|
41
|
-
const result = order.validate();
|
|
42
|
-
expect(result.ok).toBe(false);
|
|
43
|
-
expect(result.error).toContain('total must be positive');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('accepts valid order', () => {
|
|
47
|
-
const order = new Order('1', 'C1', [{ productId: 'P1', quantity: 1, price: 10 }], 'pending', 10);
|
|
48
|
-
const result = order.validate();
|
|
49
|
-
expect(result.ok).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('business rules', () => {
|
|
54
|
-
it('cannot cancel shipped order', () => {
|
|
55
|
-
const order = new Order('1', 'C1', [], 'shipped', 0);
|
|
56
|
-
expect(order.canCancel()).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('can cancel pending order', () => {
|
|
60
|
-
const order = new Order('1', 'C1', [], 'pending', 0);
|
|
61
|
-
expect(order.canCancel()).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('calculates discount correctly', () => {
|
|
65
|
-
const order = new Order('1', 'C1', [], 'pending', 100);
|
|
66
|
-
const discounted = order.applyDiscount(0.1); // 10% discount
|
|
67
|
-
expect(discounted.total).toBe(90);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('transformations', () => {
|
|
72
|
-
it('converts request to entity with calculated total', () => {
|
|
73
|
-
const order = Order.fromRequest({
|
|
74
|
-
customerId: 'C1',
|
|
75
|
-
items: [
|
|
76
|
-
{ productId: 'P1', quantity: 2, price: 10 },
|
|
77
|
-
{ productId: 'P2', quantity: 1, price: 15 }
|
|
78
|
-
]
|
|
79
|
-
});
|
|
80
|
-
expect(order.customerId).toBe('C1');
|
|
81
|
-
expect(order.total).toBe(35);
|
|
82
|
-
expect(order.status).toBe('pending');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('converts entity to database record', () => {
|
|
86
|
-
const order = new Order('123', 'C1', [{ productId: 'P1', quantity: 2, price: 10 }], 'pending', 20);
|
|
87
|
-
const record = order.toRecord();
|
|
88
|
-
|
|
89
|
-
expect(record.id).toBe('123');
|
|
90
|
-
expect(record.customer_id).toBe('C1');
|
|
91
|
-
expect(record.status).toBe('pending');
|
|
92
|
-
expect(record.items).toBe(JSON.stringify([{ productId: 'P1', quantity: 2, price: 10 }]));
|
|
93
|
-
expect(record.total).toBe(20);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('converts database record back to entity', () => {
|
|
97
|
-
const record = {
|
|
98
|
-
id: '123',
|
|
99
|
-
customer_id: 'C1',
|
|
100
|
-
items: '[{"productId":"P1","quantity":2,"price":10}]',
|
|
101
|
-
status: 'pending',
|
|
102
|
-
total: 20
|
|
103
|
-
};
|
|
104
|
-
const order = Order.fromRecord(record);
|
|
105
|
-
|
|
106
|
-
expect(order.id).toBe('123');
|
|
107
|
-
expect(order.customerId).toBe('C1');
|
|
108
|
-
expect(order.items).toHaveLength(1);
|
|
109
|
-
expect(order.items[0].productId).toBe('P1');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('converts entity to API response', () => {
|
|
113
|
-
const order = new Order('123', 'C1', [{ productId: 'P1', quantity: 2, price: 10 }], 'pending', 20);
|
|
114
|
-
const response = order.toResponse();
|
|
115
|
-
|
|
116
|
-
expect(response.id).toBe('123');
|
|
117
|
-
expect(response.customerId).toBe('C1');
|
|
118
|
-
expect(response.status).toBe('pending');
|
|
119
|
-
expect(response.total).toBe(20);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('invariants', () => {
|
|
124
|
-
it('maintains total consistency with items', () => {
|
|
125
|
-
const order = new Order('1', 'C1', [{ productId: 'P1', quantity: 2, price: 10 }], 'pending', 20);
|
|
126
|
-
|
|
127
|
-
// Invariant: total always matches item calculation
|
|
128
|
-
const calculated = order.items.reduce((sum, item) => sum + item.quantity * item.price, 0);
|
|
129
|
-
expect(order.total).toBe(calculated);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('prevents invalid status transitions', () => {
|
|
133
|
-
const order = new Order('1', 'C1', [], 'shipped', 0);
|
|
134
|
-
|
|
135
|
-
// Cannot go backwards
|
|
136
|
-
expect(() => order.updateStatus('pending')).toThrow('Invalid status transition');
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Service Boundary (Functional Core)
|
|
143
|
-
|
|
144
|
-
Orchestration logic with stubbed dependencies.
|
|
145
|
-
|
|
146
|
-
### What to Test at Service Boundary
|
|
147
|
-
|
|
148
|
-
- Business workflow execution
|
|
149
|
-
- Orchestration of multiple entities
|
|
150
|
-
- Error propagation and handling
|
|
151
|
-
- Repository interaction patterns
|
|
152
|
-
- Validation and state transitions
|
|
153
|
-
|
|
154
|
-
### Service Test Examples
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
describe('OrderService', () => {
|
|
158
|
-
let service: OrderService;
|
|
159
|
-
let mockOrderRepo: OrderRepository;
|
|
160
|
-
let mockInventoryRepo: InventoryRepository;
|
|
161
|
-
|
|
162
|
-
beforeEach(() => {
|
|
163
|
-
mockOrderRepo = {
|
|
164
|
-
save: vi.fn(),
|
|
165
|
-
findById: vi.fn(),
|
|
166
|
-
delete: vi.fn()
|
|
167
|
-
};
|
|
168
|
-
mockInventoryRepo = {
|
|
169
|
-
checkAvailability: vi.fn(),
|
|
170
|
-
reserve: vi.fn(),
|
|
171
|
-
release: vi.fn()
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
service = new OrderService(mockOrderRepo, mockInventoryRepo);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe('createOrder', () => {
|
|
178
|
-
it('creates order with valid data', async () => {
|
|
179
|
-
mockInventoryRepo.checkAvailability.mockResolvedValue({ available: true });
|
|
180
|
-
mockOrderRepo.save.mockResolvedValue({ id: '123' });
|
|
181
|
-
|
|
182
|
-
const result = await service.createOrder({
|
|
183
|
-
customerId: 'C1',
|
|
184
|
-
items: [{ productId: 'P1', quantity: 2 }]
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
expect(result.ok).toBe(true);
|
|
188
|
-
expect(mockOrderRepo.save).toHaveBeenCalled();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('checks inventory before saving', async () => {
|
|
192
|
-
mockInventoryRepo.checkAvailability.mockResolvedValue({ available: true });
|
|
193
|
-
mockOrderRepo.save.mockResolvedValue({ id: '123' });
|
|
194
|
-
|
|
195
|
-
await service.createOrder({
|
|
196
|
-
customerId: 'C1',
|
|
197
|
-
items: [
|
|
198
|
-
{ productId: 'P1', quantity: 2 },
|
|
199
|
-
{ productId: 'P2', quantity: 3 }
|
|
200
|
-
]
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
expect(mockInventoryRepo.checkAvailability).toHaveBeenCalledWith(
|
|
204
|
-
expect.arrayContaining([
|
|
205
|
-
expect.objectContaining({ productId: 'P1', quantity: 2 }),
|
|
206
|
-
expect.objectContaining({ productId: 'P2', quantity: 3 })
|
|
207
|
-
])
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('does not save order when inventory unavailable', async () => {
|
|
212
|
-
mockInventoryRepo.checkAvailability.mockResolvedValue({ available: false });
|
|
213
|
-
|
|
214
|
-
const result = await service.createOrder({
|
|
215
|
-
customerId: 'C1',
|
|
216
|
-
items: [{ productId: 'P1', quantity: 100 }]
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
expect(result.ok).toBe(false);
|
|
220
|
-
expect(mockOrderRepo.save).not.toHaveBeenCalled();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('reverses inventory reservation on save failure', async () => {
|
|
224
|
-
mockInventoryRepo.checkAvailability.mockResolvedValue({ available: true });
|
|
225
|
-
mockOrderRepo.save.mockRejectedValue(new Error('DB error'));
|
|
226
|
-
|
|
227
|
-
const result = await service.createOrder({
|
|
228
|
-
customerId: 'C1',
|
|
229
|
-
items: [{ productId: 'P1', quantity: 2 }]
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
expect(result.ok).toBe(false);
|
|
233
|
-
expect(mockInventoryRepo.release).toHaveBeenCalled();
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
describe('cancelOrder', () => {
|
|
238
|
-
it('finds order and cancels it', async () => {
|
|
239
|
-
mockOrderRepo.findById.mockResolvedValue({ id: '123', status: 'pending' });
|
|
240
|
-
mockOrderRepo.save.mockResolvedValue({ id: '123', status: 'cancelled' });
|
|
241
|
-
|
|
242
|
-
const result = await service.cancelOrder('123');
|
|
243
|
-
|
|
244
|
-
expect(result.ok).toBe(true);
|
|
245
|
-
expect(mockOrderRepo.save).toHaveBeenCalled();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('releases inventory when cancelling', async () => {
|
|
249
|
-
mockOrderRepo.findById.mockResolvedValue({
|
|
250
|
-
id: '123',
|
|
251
|
-
status: 'pending',
|
|
252
|
-
items: [{ productId: 'P1', quantity: 2 }]
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
await service.cancelOrder('123');
|
|
256
|
-
|
|
257
|
-
expect(mockInventoryRepo.release).toHaveBeenCalledWith(
|
|
258
|
-
expect.arrayContaining([
|
|
259
|
-
expect.objectContaining({ productId: 'P1', quantity: 2 })
|
|
260
|
-
])
|
|
261
|
-
);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('returns error if order not found', async () => {
|
|
265
|
-
mockOrderRepo.findById.mockResolvedValue(null);
|
|
266
|
-
|
|
267
|
-
const result = await service.cancelOrder('999');
|
|
268
|
-
|
|
269
|
-
expect(result.ok).toBe(false);
|
|
270
|
-
expect(mockOrderRepo.save).not.toHaveBeenCalled();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('returns error if order cannot be cancelled', async () => {
|
|
274
|
-
mockOrderRepo.findById.mockResolvedValue({ id: '123', status: 'shipped' });
|
|
275
|
-
|
|
276
|
-
const result = await service.cancelOrder('123');
|
|
277
|
-
|
|
278
|
-
expect(result.ok).toBe(false);
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
## Router Boundary (Effectful Edge)
|
|
285
|
-
|
|
286
|
-
HTTP request/response handling.
|
|
287
|
-
|
|
288
|
-
### What to Test at Router Boundary
|
|
289
|
-
|
|
290
|
-
- Request parsing and validation
|
|
291
|
-
- Response status codes and format
|
|
292
|
-
- Error response handling
|
|
293
|
-
- Request/response transformations
|
|
294
|
-
- HTTP headers and content types
|
|
295
|
-
|
|
296
|
-
### Router Test Examples
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
describe('Order API', () => {
|
|
300
|
-
let app: Express;
|
|
301
|
-
let mockService: OrderService;
|
|
302
|
-
|
|
303
|
-
beforeEach(() => {
|
|
304
|
-
mockService = {
|
|
305
|
-
createOrder: vi.fn(),
|
|
306
|
-
cancelOrder: vi.fn(),
|
|
307
|
-
getOrder: vi.fn()
|
|
308
|
-
};
|
|
309
|
-
app = createApp(mockService);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
describe('POST /orders', () => {
|
|
313
|
-
it('returns 201 for valid request', async () => {
|
|
314
|
-
mockService.createOrder.mockResolvedValue(
|
|
315
|
-
Ok(new Order('123', 'C1', [], 'pending', 0))
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const response = await request(app)
|
|
319
|
-
.post('/orders')
|
|
320
|
-
.send({ customerId: 'C1', items: [{ productId: 'P1', quantity: 2 }] });
|
|
321
|
-
|
|
322
|
-
expect(response.status).toBe(201);
|
|
323
|
-
expect(response.body.id).toBe('123');
|
|
324
|
-
expect(response.body.status).toBe('pending');
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('returns 400 for invalid request body', async () => {
|
|
328
|
-
const response = await request(app)
|
|
329
|
-
.post('/orders')
|
|
330
|
-
.send({ customerId: 'C1' }); // Missing items
|
|
331
|
-
|
|
332
|
-
expect(response.status).toBe(400);
|
|
333
|
-
expect(response.body.error).toBeDefined();
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('returns 400 when service returns error', async () => {
|
|
337
|
-
mockService.createOrder.mockResolvedValue(
|
|
338
|
-
Err('Invalid order')
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
const response = await request(app)
|
|
342
|
-
.post('/orders')
|
|
343
|
-
.send({ customerId: 'C1', items: [] });
|
|
344
|
-
|
|
345
|
-
expect(response.status).toBe(400);
|
|
346
|
-
expect(response.body.error).toBe('Invalid order');
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('includes correct content-type header', async () => {
|
|
350
|
-
mockService.createOrder.mockResolvedValue(
|
|
351
|
-
Ok(new Order('123', 'C1', [], 'pending', 0))
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
const response = await request(app)
|
|
355
|
-
.post('/orders')
|
|
356
|
-
.send({ customerId: 'C1', items: [{ productId: 'P1', quantity: 2 }] });
|
|
357
|
-
|
|
358
|
-
expect(response.headers['content-type']).toMatch(/application\/json/);
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
describe('GET /orders/:id', () => {
|
|
363
|
-
it('returns 200 with order', async () => {
|
|
364
|
-
mockService.getOrder.mockResolvedValue(
|
|
365
|
-
Ok(new Order('123', 'C1', [], 'pending', 0))
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
const response = await request(app)
|
|
369
|
-
.get('/orders/123');
|
|
370
|
-
|
|
371
|
-
expect(response.status).toBe(200);
|
|
372
|
-
expect(response.body.id).toBe('123');
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it('returns 404 when order not found', async () => {
|
|
376
|
-
mockService.getOrder.mockResolvedValue(
|
|
377
|
-
Err('Order not found')
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
const response = await request(app)
|
|
381
|
-
.get('/orders/999');
|
|
382
|
-
|
|
383
|
-
expect(response.status).toBe(404);
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
describe('DELETE /orders/:id', () => {
|
|
388
|
-
it('returns 204 on successful cancel', async () => {
|
|
389
|
-
mockService.cancelOrder.mockResolvedValue(Ok(undefined));
|
|
390
|
-
|
|
391
|
-
const response = await request(app)
|
|
392
|
-
.delete('/orders/123');
|
|
393
|
-
|
|
394
|
-
expect(response.status).toBe(204);
|
|
395
|
-
expect(response.body).toEqual({});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it('returns 400 when cancel fails', async () => {
|
|
399
|
-
mockService.cancelOrder.mockResolvedValue(
|
|
400
|
-
Err('Cannot cancel shipped order')
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
const response = await request(app)
|
|
404
|
-
.delete('/orders/123');
|
|
405
|
-
|
|
406
|
-
expect(response.status).toBe(400);
|
|
407
|
-
expect(response.body.error).toBe('Cannot cancel shipped order');
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
## Repository Boundary (Effectful Edge)
|
|
414
|
-
|
|
415
|
-
Database operations.
|
|
416
|
-
|
|
417
|
-
### What to Test at Repository Boundary
|
|
418
|
-
|
|
419
|
-
- Save operations preserve data
|
|
420
|
-
- Retrieval returns correct data
|
|
421
|
-
- Updates modify existing records
|
|
422
|
-
- Deletes remove records
|
|
423
|
-
- Complex queries return expected results
|
|
424
|
-
- Transaction handling
|
|
425
|
-
|
|
426
|
-
### Repository Test Examples
|
|
427
|
-
|
|
428
|
-
```typescript
|
|
429
|
-
describe('OrderRepository', () => {
|
|
430
|
-
let repo: OrderRepository;
|
|
431
|
-
let testDb: Database;
|
|
432
|
-
|
|
433
|
-
beforeEach(async () => {
|
|
434
|
-
testDb = await createTestDatabase();
|
|
435
|
-
repo = new OrderRepository(testDb);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
afterEach(async () => {
|
|
439
|
-
await testDb.close();
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
describe('save', () => {
|
|
443
|
-
it('saves order to database', async () => {
|
|
444
|
-
const record = {
|
|
445
|
-
id: '123',
|
|
446
|
-
customer_id: 'C1',
|
|
447
|
-
items: JSON.stringify([{ productId: 'P1', quantity: 2 }]),
|
|
448
|
-
status: 'pending',
|
|
449
|
-
total: 20
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const saved = await repo.save(record);
|
|
453
|
-
|
|
454
|
-
expect(saved.id).toBe('123');
|
|
455
|
-
expect(saved.customer_id).toBe('C1');
|
|
456
|
-
|
|
457
|
-
// Verify in database
|
|
458
|
-
const found = await testDb.orders.findOne({ id: '123' });
|
|
459
|
-
expect(found).toBeDefined();
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it('updates existing order', async () => {
|
|
463
|
-
const record = {
|
|
464
|
-
id: '123',
|
|
465
|
-
customer_id: 'C1',
|
|
466
|
-
items: JSON.stringify([]),
|
|
467
|
-
status: 'pending',
|
|
468
|
-
total: 0
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
await repo.save(record);
|
|
472
|
-
|
|
473
|
-
const updated = { ...record, status: 'shipped' };
|
|
474
|
-
await repo.save(updated);
|
|
475
|
-
|
|
476
|
-
const found = await testDb.orders.findOne({ id: '123' });
|
|
477
|
-
expect(found.status).toBe('shipped');
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
describe('findById', () => {
|
|
482
|
-
it('retrieves order by id', async () => {
|
|
483
|
-
const record = {
|
|
484
|
-
id: '123',
|
|
485
|
-
customer_id: 'C1',
|
|
486
|
-
items: JSON.stringify([]),
|
|
487
|
-
status: 'pending',
|
|
488
|
-
total: 0
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
await repo.save(record);
|
|
492
|
-
const found = await repo.findById('123');
|
|
493
|
-
|
|
494
|
-
expect(found).toEqual(record);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
it('returns null for non-existent id', async () => {
|
|
498
|
-
const found = await repo.findById('999');
|
|
499
|
-
|
|
500
|
-
expect(found).toBeNull();
|
|
501
|
-
});
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
describe('delete', () => {
|
|
505
|
-
it('removes order from database', async () => {
|
|
506
|
-
const record = {
|
|
507
|
-
id: '123',
|
|
508
|
-
customer_id: 'C1',
|
|
509
|
-
items: JSON.stringify([]),
|
|
510
|
-
status: 'pending',
|
|
511
|
-
total: 0
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
await repo.save(record);
|
|
515
|
-
await repo.delete('123');
|
|
516
|
-
|
|
517
|
-
const found = await testDb.orders.findOne({ id: '123' });
|
|
518
|
-
expect(found).toBeUndefined();
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
## Consumer Boundary (Effectful Edge)
|
|
525
|
-
|
|
526
|
-
Event message handling.
|
|
527
|
-
|
|
528
|
-
### Consumer Test Examples
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
describe('OrderConsumer', () => {
|
|
532
|
-
let consumer: OrderConsumer;
|
|
533
|
-
let mockService: OrderService;
|
|
534
|
-
|
|
535
|
-
beforeEach(() => {
|
|
536
|
-
mockService = {
|
|
537
|
-
processOrder: vi.fn(),
|
|
538
|
-
handlePayment: vi.fn()
|
|
539
|
-
};
|
|
540
|
-
consumer = new OrderConsumer(mockService);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
describe('OrderPlaced event', () => {
|
|
544
|
-
it('calls service with event data', async () => {
|
|
545
|
-
mockService.processOrder.mockResolvedValue(Ok({}));
|
|
546
|
-
|
|
547
|
-
const event = {
|
|
548
|
-
type: 'OrderPlaced',
|
|
549
|
-
data: { orderId: '123', customerId: 'C1' }
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
await consumer.handle(event);
|
|
553
|
-
|
|
554
|
-
expect(mockService.processOrder).toHaveBeenCalledWith('123');
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
it('ignores unknown event types', async () => {
|
|
558
|
-
const event = {
|
|
559
|
-
type: 'UnknownEvent',
|
|
560
|
-
data: {}
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
await consumer.handle(event);
|
|
564
|
-
|
|
565
|
-
expect(mockService.processOrder).not.toHaveBeenCalled();
|
|
566
|
-
});
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
## Summary Table
|
|
572
|
-
|
|
573
|
-
| Boundary | Test Type | What to Stub | What to Assert |
|
|
574
|
-
|----------|-----------|--------------|-----------------|
|
|
575
|
-
| **Entity** | Unit | Nothing (pure) | Validation, rules, transforms |
|
|
576
|
-
| **Service** | Unit | Repositories, other services | Orchestration logic, error handling |
|
|
577
|
-
| **Router** | Integration | Service | Status codes, response format, headers |
|
|
578
|
-
| **Repository** | Integration | Database connection | CRUD operations, queries |
|
|
579
|
-
| **Consumer** | Integration | Service, event source | Event parsing, service calls |
|