@martinffx/atelier 0.1.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/agents/architect.md +38 -0
- package/agents/oracle.md +40 -0
- package/agents/recon.md +39 -0
- package/dist/atelier.js +33082 -0
- package/package.json +44 -0
- package/skills/code-conventional-commit/SKILL.md +134 -0
- package/skills/code-conventional-commit/evals/evals.json +23 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/commit-message.txt +4 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/validation-result.txt +16 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/commit-message.txt +8 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/report.html +1325 -0
- package/skills/code-debug/SKILL.md +271 -0
- package/skills/code-debug/references/language-tools.md +139 -0
- package/skills/code-debug/references/techniques.md +117 -0
- package/skills/code-docs/SKILL.md +171 -0
- package/skills/code-docs/references/api-docs.md +116 -0
- package/skills/code-docs/references/changelog.md +64 -0
- package/skills/code-docs/references/readme-structure.md +94 -0
- package/skills/code-git-worktrees/SKILL.md +218 -0
- package/skills/code-perf/SKILL.md +163 -0
- package/skills/code-perf/references/common-bottlenecks.md +124 -0
- package/skills/code-perf/references/optimization-patterns.md +167 -0
- package/skills/code-perf/references/profiling-tools.md +126 -0
- package/skills/code-review/SKILL.md +78 -0
- package/skills/code-review/references/output.md +200 -0
- package/skills/code-review/references/reviewers.md +719 -0
- package/skills/code-review/references/rq.md +357 -0
- package/skills/code-review/references/rs.md +30 -0
- package/skills/code-security/SKILL.md +183 -0
- package/skills/code-security/references/owasp-top-10.md +129 -0
- package/skills/code-security/references/security-tools.md +102 -0
- package/skills/code-security/references/vulnerability-patterns.md +129 -0
- package/skills/code-stacked-commit/SKILL.md +155 -0
- package/skills/code-subagents/SKILL.md +184 -0
- package/skills/code-subagents/references/code-quality-reviewer-prompt.md +48 -0
- package/skills/code-subagents/references/implementor-prompt.md +79 -0
- package/skills/code-subagents/references/spec-reviewer-prompt.md +62 -0
- package/skills/oracle-architect/SKILL.md +246 -0
- package/skills/oracle-architect/references/api-design.md +345 -0
- package/skills/oracle-architect/references/data-modeling.md +318 -0
- package/skills/oracle-architect/references/ddd-patterns.md +247 -0
- package/skills/oracle-challenge/SKILL.md +126 -0
- package/skills/oracle-testing/SKILL.md +237 -0
- package/skills/oracle-testing/references/anti-patterns.md +618 -0
- package/skills/oracle-testing/references/boundaries.md +579 -0
- package/skills/oracle-testing/references/core-testing.md +691 -0
- package/skills/oracle-testing/references/edge-testing.md +794 -0
- package/skills/oracle-testing/references/stub-driven-tdd.md +322 -0
- package/skills/oracle-thinkdeep/SKILL.md +164 -0
- package/skills/python-architecture/SKILL.md +148 -0
- package/skills/python-architecture/references/data-modeling.md +558 -0
- package/skills/python-architecture/references/ddd.md +569 -0
- package/skills/python-architecture/references/functional-core.md +337 -0
- package/skills/python-build-tools/SKILL.md +153 -0
- package/skills/python-build-tools/references/basedpyright.md +321 -0
- package/skills/python-build-tools/references/ruff.md +225 -0
- package/skills/python-build-tools/references/uv.md +257 -0
- package/skills/python-fastapi/SKILL.md +184 -0
- package/skills/python-fastapi/references/api-design.md +563 -0
- package/skills/python-fastapi/references/dependencies.md +479 -0
- package/skills/python-fastapi/references/middleware.md +475 -0
- package/skills/python-fastapi/references/validation.md +497 -0
- package/skills/python-modern-python/SKILL.md +201 -0
- package/skills/python-modern-python/references/async.md +76 -0
- package/skills/python-modern-python/references/pattern-matching.md +89 -0
- package/skills/python-modern-python/references/typing.md +96 -0
- package/skills/python-monorepo/SKILL.md +146 -0
- package/skills/python-monorepo/references/docker.md +515 -0
- package/skills/python-monorepo/references/namespace-packages.md +426 -0
- package/skills/python-monorepo/references/workspace-config.md +341 -0
- package/skills/python-sqlalchemy/SKILL.md +119 -0
- package/skills/python-sqlalchemy/references/async.md +51 -0
- package/skills/python-sqlalchemy/references/models.md +67 -0
- package/skills/python-sqlalchemy/references/queries.md +57 -0
- package/skills/python-temporal/SKILL.md +128 -0
- package/skills/python-temporal/references/patterns.md +87 -0
- package/skills/python-temporal/references/testing.md +54 -0
- package/skills/python-testing/SKILL.md +161 -0
- package/skills/python-testing/references/boundaries.md +449 -0
- package/skills/python-testing/references/mocking.md +449 -0
- package/skills/python-testing/references/pytest.md +524 -0
- package/skills/spec-brainstorm/SKILL.md +325 -0
- package/skills/spec-finish/SKILL.md +159 -0
- package/skills/spec-implement/SKILL.md +296 -0
- package/skills/spec-orchestrator/SKILL.md +156 -0
- package/skills/spec-plan/SKILL.md +248 -0
- package/skills/typescript-api-design/SKILL.md +396 -0
- package/skills/typescript-api-design/references/error-responses.md +469 -0
- package/skills/typescript-build-tools/SKILL.md +447 -0
- package/skills/typescript-build-tools/references/biome.md +436 -0
- package/skills/typescript-build-tools/references/bun.md +432 -0
- package/skills/typescript-build-tools/references/turborepo.md +426 -0
- package/skills/typescript-build-tools/references/vitest.md +518 -0
- package/skills/typescript-drizzle-orm/SKILL.md +398 -0
- package/skills/typescript-drizzle-orm/references/cloudflare.md +392 -0
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +412 -0
- package/skills/typescript-drizzle-orm/references/postgresql.md +191 -0
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +479 -0
- package/skills/typescript-drizzle-orm/references/sqlite.md +291 -0
- package/skills/typescript-dynamodb-toolbox/SKILL.md +370 -0
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +574 -0
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +495 -0
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +332 -0
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +631 -0
- package/skills/typescript-dynamodb-toolbox/references/testing.md +718 -0
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +423 -0
- package/skills/typescript-effect-ts/SKILL.md +341 -0
- package/skills/typescript-effect-ts/references/core.md +412 -0
- package/skills/typescript-effect-ts/references/error-handling.md +468 -0
- package/skills/typescript-effect-ts/references/resources.md +476 -0
- package/skills/typescript-effect-ts/references/schema.md +536 -0
- package/skills/typescript-effect-ts/references/services.md +461 -0
- package/skills/typescript-fastify/SKILL.md +405 -0
- package/skills/typescript-fastify/references/plugins.md +329 -0
- package/skills/typescript-fastify/references/typeid.md +315 -0
- package/skills/typescript-functional-patterns/SKILL.md +402 -0
- package/skills/typescript-functional-patterns/references/adts.md +497 -0
- package/skills/typescript-functional-patterns/references/branded-types.md +542 -0
- package/skills/typescript-functional-patterns/references/migration-guide.md +695 -0
- package/skills/typescript-functional-patterns/references/option-result.md +585 -0
- package/skills/typescript-testing/SKILL.md +353 -0
- package/skills/typescript-testing/references/mocking.md +675 -0
- package/skills/typescript-testing/references/msw.md +852 -0
- package/skills/typescript-testing/references/snapshot-testing.md +774 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
# Edge Testing Patterns
|
|
2
|
+
|
|
3
|
+
Router, Repository, Consumer, Producer, and Client integration test patterns.
|
|
4
|
+
|
|
5
|
+
## Router Integration Tests
|
|
6
|
+
|
|
7
|
+
Test HTTP request/response handling with real HTTP but stubbed service.
|
|
8
|
+
|
|
9
|
+
### What to Test
|
|
10
|
+
|
|
11
|
+
- Request parsing and validation
|
|
12
|
+
- Response status codes (200, 201, 400, 404, 500)
|
|
13
|
+
- Response body structure and format
|
|
14
|
+
- Error response handling
|
|
15
|
+
- HTTP headers (content-type, cache-control, etc.)
|
|
16
|
+
- Request/response transformations
|
|
17
|
+
|
|
18
|
+
### Router Test Setup
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
22
|
+
import request from 'supertest';
|
|
23
|
+
import { Express } from 'express';
|
|
24
|
+
import { createApp } from './app';
|
|
25
|
+
import { OrderService } from './order-service';
|
|
26
|
+
import { vi } from 'vitest';
|
|
27
|
+
import { Ok, Err } from './result';
|
|
28
|
+
import { Order } from './order';
|
|
29
|
+
|
|
30
|
+
describe('Order API', () => {
|
|
31
|
+
let app: Express;
|
|
32
|
+
let mockService: OrderService;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
// Create mock service
|
|
36
|
+
mockService = {
|
|
37
|
+
createOrder: vi.fn(),
|
|
38
|
+
getOrder: vi.fn(),
|
|
39
|
+
cancelOrder: vi.fn(),
|
|
40
|
+
getCustomerOrders: vi.fn()
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Create app with mocked service
|
|
44
|
+
app = createApp(mockService);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### POST Endpoint Tests
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
describe('POST /orders', () => {
|
|
53
|
+
it('returns 201 with order for valid request', async () => {
|
|
54
|
+
mockService.createOrder.mockResolvedValue(
|
|
55
|
+
Ok(new Order('123', 'C1', [{ productId: 'P1', quantity: 2, price: 10 }], 'pending', 20))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const response = await request(app)
|
|
59
|
+
.post('/orders')
|
|
60
|
+
.send({
|
|
61
|
+
customerId: 'C1',
|
|
62
|
+
items: [{ productId: 'P1', quantity: 2 }]
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(response.status).toBe(201);
|
|
66
|
+
expect(response.body).toHaveProperty('id');
|
|
67
|
+
expect(response.body.id).toBe('123');
|
|
68
|
+
expect(response.body.customerId).toBe('C1');
|
|
69
|
+
expect(response.body.status).toBe('pending');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns 400 for missing required field', async () => {
|
|
73
|
+
const response = await request(app)
|
|
74
|
+
.post('/orders')
|
|
75
|
+
.send({
|
|
76
|
+
// customerId missing
|
|
77
|
+
items: [{ productId: 'P1', quantity: 2 }]
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(response.status).toBe(400);
|
|
81
|
+
expect(response.body).toHaveProperty('error');
|
|
82
|
+
expect(response.body.error).toContain('customerId');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns 400 for empty items array', async () => {
|
|
86
|
+
const response = await request(app)
|
|
87
|
+
.post('/orders')
|
|
88
|
+
.send({
|
|
89
|
+
customerId: 'C1',
|
|
90
|
+
items: []
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(response.status).toBe(400);
|
|
94
|
+
expect(response.body.error).toContain('items');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns 400 when service returns error', async () => {
|
|
98
|
+
mockService.createOrder.mockResolvedValue(
|
|
99
|
+
Err('Invalid order')
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const response = await request(app)
|
|
103
|
+
.post('/orders')
|
|
104
|
+
.send({
|
|
105
|
+
customerId: 'C1',
|
|
106
|
+
items: [{ productId: 'P1', quantity: 2 }]
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(response.status).toBe(400);
|
|
110
|
+
expect(response.body.error).toBe('Invalid order');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('sets correct content-type header', async () => {
|
|
114
|
+
mockService.createOrder.mockResolvedValue(
|
|
115
|
+
Ok(new Order('123', 'C1', [], 'pending', 0))
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const response = await request(app)
|
|
119
|
+
.post('/orders')
|
|
120
|
+
.send({
|
|
121
|
+
customerId: 'C1',
|
|
122
|
+
items: [{ productId: 'P1', quantity: 2 }]
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(response.headers['content-type']).toMatch(/application\/json/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('includes location header with resource URL', async () => {
|
|
129
|
+
mockService.createOrder.mockResolvedValue(
|
|
130
|
+
Ok(new Order('123', 'C1', [], 'pending', 0))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const response = await request(app)
|
|
134
|
+
.post('/orders')
|
|
135
|
+
.send({
|
|
136
|
+
customerId: 'C1',
|
|
137
|
+
items: [{ productId: 'P1', quantity: 2 }]
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(response.headers['location']).toBe('/orders/123');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('handles service errors gracefully', async () => {
|
|
144
|
+
mockService.createOrder.mockRejectedValue(new Error('Database error'));
|
|
145
|
+
|
|
146
|
+
const response = await request(app)
|
|
147
|
+
.post('/orders')
|
|
148
|
+
.send({
|
|
149
|
+
customerId: 'C1',
|
|
150
|
+
items: [{ productId: 'P1', quantity: 2 }]
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(response.status).toBe(500);
|
|
154
|
+
expect(response.body).toHaveProperty('error');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### GET Endpoint Tests
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
describe('GET /orders/:id', () => {
|
|
163
|
+
it('returns 200 with order', async () => {
|
|
164
|
+
mockService.getOrder.mockResolvedValue(
|
|
165
|
+
Ok(new Order('123', 'C1', [{ productId: 'P1', quantity: 2, price: 10 }], 'pending', 20))
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const response = await request(app)
|
|
169
|
+
.get('/orders/123');
|
|
170
|
+
|
|
171
|
+
expect(response.status).toBe(200);
|
|
172
|
+
expect(response.body.id).toBe('123');
|
|
173
|
+
expect(response.body.customerId).toBe('C1');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('returns 404 when order not found', async () => {
|
|
177
|
+
mockService.getOrder.mockResolvedValue(
|
|
178
|
+
Err('Order not found')
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const response = await request(app)
|
|
182
|
+
.get('/orders/999');
|
|
183
|
+
|
|
184
|
+
expect(response.status).toBe(404);
|
|
185
|
+
expect(response.body.error).toBe('Order not found');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('includes cache control headers', async () => {
|
|
189
|
+
mockService.getOrder.mockResolvedValue(
|
|
190
|
+
Ok(new Order('123', 'C1', [], 'pending', 0))
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const response = await request(app)
|
|
194
|
+
.get('/orders/123');
|
|
195
|
+
|
|
196
|
+
expect(response.headers['cache-control']).toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('validates order id parameter', async () => {
|
|
200
|
+
const response = await request(app)
|
|
201
|
+
.get('/orders/invalid-uuid-format');
|
|
202
|
+
|
|
203
|
+
expect(response.status).toBe(400);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('GET /customers/:customerId/orders', () => {
|
|
208
|
+
it('returns 200 with array of orders', async () => {
|
|
209
|
+
mockService.getCustomerOrders.mockResolvedValue(
|
|
210
|
+
Ok([
|
|
211
|
+
new Order('123', 'C1', [], 'pending', 0),
|
|
212
|
+
new Order('124', 'C1', [], 'shipped', 50)
|
|
213
|
+
])
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const response = await request(app)
|
|
217
|
+
.get('/customers/C1/orders');
|
|
218
|
+
|
|
219
|
+
expect(response.status).toBe(200);
|
|
220
|
+
expect(Array.isArray(response.body)).toBe(true);
|
|
221
|
+
expect(response.body).toHaveLength(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('returns empty array for customer with no orders', async () => {
|
|
225
|
+
mockService.getCustomerOrders.mockResolvedValue(Ok([]));
|
|
226
|
+
|
|
227
|
+
const response = await request(app)
|
|
228
|
+
.get('/customers/C999/orders');
|
|
229
|
+
|
|
230
|
+
expect(response.status).toBe(200);
|
|
231
|
+
expect(response.body).toEqual([]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### DELETE Endpoint Tests
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
describe('DELETE /orders/:id', () => {
|
|
240
|
+
it('returns 204 on successful cancel', async () => {
|
|
241
|
+
mockService.cancelOrder.mockResolvedValue(Ok(undefined));
|
|
242
|
+
|
|
243
|
+
const response = await request(app)
|
|
244
|
+
.delete('/orders/123');
|
|
245
|
+
|
|
246
|
+
expect(response.status).toBe(204);
|
|
247
|
+
expect(Object.keys(response.body)).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns 400 when cancel fails', async () => {
|
|
251
|
+
mockService.cancelOrder.mockResolvedValue(
|
|
252
|
+
Err('Cannot cancel shipped order')
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const response = await request(app)
|
|
256
|
+
.delete('/orders/123');
|
|
257
|
+
|
|
258
|
+
expect(response.status).toBe(400);
|
|
259
|
+
expect(response.body.error).toBe('Cannot cancel shipped order');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('returns 404 for non-existent order', async () => {
|
|
263
|
+
mockService.cancelOrder.mockResolvedValue(
|
|
264
|
+
Err('Order not found')
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const response = await request(app)
|
|
268
|
+
.delete('/orders/999');
|
|
269
|
+
|
|
270
|
+
expect(response.status).toBe(404);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('calls service with correct order id', async () => {
|
|
274
|
+
mockService.cancelOrder.mockResolvedValue(Ok(undefined));
|
|
275
|
+
|
|
276
|
+
await request(app)
|
|
277
|
+
.delete('/orders/123');
|
|
278
|
+
|
|
279
|
+
expect(mockService.cancelOrder).toHaveBeenCalledWith('123');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Repository Integration Tests
|
|
285
|
+
|
|
286
|
+
Test database operations with real test database.
|
|
287
|
+
|
|
288
|
+
### Repository Test Setup
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
292
|
+
import { OrderRepository } from './order-repository';
|
|
293
|
+
import { Database } from './database';
|
|
294
|
+
import { createTestDatabase, cleanupTestDatabase } from './test-fixtures/database';
|
|
295
|
+
|
|
296
|
+
describe('OrderRepository', () => {
|
|
297
|
+
let repo: OrderRepository;
|
|
298
|
+
let testDb: Database;
|
|
299
|
+
|
|
300
|
+
beforeEach(async () => {
|
|
301
|
+
testDb = await createTestDatabase();
|
|
302
|
+
repo = new OrderRepository(testDb);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
afterEach(async () => {
|
|
306
|
+
await cleanupTestDatabase(testDb);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### CRUD Operation Tests
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
describe('save', () => {
|
|
315
|
+
it('saves new order to database', async () => {
|
|
316
|
+
const record = {
|
|
317
|
+
id: '123',
|
|
318
|
+
customer_id: 'C1',
|
|
319
|
+
items: JSON.stringify([{ productId: 'P1', quantity: 2, price: 10 }]),
|
|
320
|
+
status: 'pending',
|
|
321
|
+
total: 20
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const saved = await repo.save(record);
|
|
325
|
+
|
|
326
|
+
expect(saved.id).toBe('123');
|
|
327
|
+
expect(saved.total).toBe(20);
|
|
328
|
+
|
|
329
|
+
// Verify persisted in database
|
|
330
|
+
const found = await testDb.orders.findOne({ id: '123' });
|
|
331
|
+
expect(found).toBeDefined();
|
|
332
|
+
expect(found.customer_id).toBe('C1');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('updates existing order', async () => {
|
|
336
|
+
const record = {
|
|
337
|
+
id: '123',
|
|
338
|
+
customer_id: 'C1',
|
|
339
|
+
items: JSON.stringify([]),
|
|
340
|
+
status: 'pending',
|
|
341
|
+
total: 0
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
await repo.save(record);
|
|
345
|
+
|
|
346
|
+
const updated = { ...record, status: 'shipped' };
|
|
347
|
+
const result = await repo.save(updated);
|
|
348
|
+
|
|
349
|
+
expect(result.status).toBe('shipped');
|
|
350
|
+
|
|
351
|
+
// Verify update persisted
|
|
352
|
+
const found = await testDb.orders.findOne({ id: '123' });
|
|
353
|
+
expect(found.status).toBe('shipped');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('preserves other fields when updating', async () => {
|
|
357
|
+
const record = {
|
|
358
|
+
id: '123',
|
|
359
|
+
customer_id: 'C1',
|
|
360
|
+
items: JSON.stringify([{ productId: 'P1', quantity: 1, price: 10 }]),
|
|
361
|
+
status: 'pending',
|
|
362
|
+
total: 10
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
await repo.save(record);
|
|
366
|
+
|
|
367
|
+
const updated = { ...record, status: 'shipped' };
|
|
368
|
+
await repo.save(updated);
|
|
369
|
+
|
|
370
|
+
const found = await testDb.orders.findOne({ id: '123' });
|
|
371
|
+
expect(found.customer_id).toBe('C1');
|
|
372
|
+
expect(found.items).toBe(JSON.stringify([{ productId: 'P1', quantity: 1, price: 10 }]));
|
|
373
|
+
expect(found.total).toBe(10);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('findById', () => {
|
|
378
|
+
it('retrieves order by id', async () => {
|
|
379
|
+
const record = {
|
|
380
|
+
id: '123',
|
|
381
|
+
customer_id: 'C1',
|
|
382
|
+
items: JSON.stringify([]),
|
|
383
|
+
status: 'pending',
|
|
384
|
+
total: 0
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
await repo.save(record);
|
|
388
|
+
const found = await repo.findById('123');
|
|
389
|
+
|
|
390
|
+
expect(found).toBeDefined();
|
|
391
|
+
expect(found.customer_id).toBe('C1');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('returns null for non-existent id', async () => {
|
|
395
|
+
const found = await repo.findById('non-existent');
|
|
396
|
+
|
|
397
|
+
expect(found).toBeNull();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('parses JSON fields correctly', async () => {
|
|
401
|
+
const items = [
|
|
402
|
+
{ productId: 'P1', quantity: 2, price: 10 },
|
|
403
|
+
{ productId: 'P2', quantity: 1, price: 15 }
|
|
404
|
+
];
|
|
405
|
+
const record = {
|
|
406
|
+
id: '123',
|
|
407
|
+
customer_id: 'C1',
|
|
408
|
+
items: JSON.stringify(items),
|
|
409
|
+
status: 'pending',
|
|
410
|
+
total: 35
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
await repo.save(record);
|
|
414
|
+
const found = await repo.findById('123');
|
|
415
|
+
|
|
416
|
+
// Verify JSON parsing
|
|
417
|
+
const parsedItems = JSON.parse(found.items);
|
|
418
|
+
expect(parsedItems).toEqual(items);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('delete', () => {
|
|
423
|
+
it('removes order from database', async () => {
|
|
424
|
+
const record = {
|
|
425
|
+
id: '123',
|
|
426
|
+
customer_id: 'C1',
|
|
427
|
+
items: JSON.stringify([]),
|
|
428
|
+
status: 'pending',
|
|
429
|
+
total: 0
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
await repo.save(record);
|
|
433
|
+
await repo.delete('123');
|
|
434
|
+
|
|
435
|
+
const found = await testDb.orders.findOne({ id: '123' });
|
|
436
|
+
expect(found).toBeUndefined();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('does not error if deleting non-existent order', async () => {
|
|
440
|
+
expect(async () => {
|
|
441
|
+
await repo.delete('non-existent');
|
|
442
|
+
}).not.toThrow();
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Query Tests
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
describe('findByCustomerId', () => {
|
|
451
|
+
beforeEach(async () => {
|
|
452
|
+
// Seed database
|
|
453
|
+
await repo.save({
|
|
454
|
+
id: '123',
|
|
455
|
+
customer_id: 'C1',
|
|
456
|
+
items: JSON.stringify([]),
|
|
457
|
+
status: 'pending',
|
|
458
|
+
total: 0
|
|
459
|
+
});
|
|
460
|
+
await repo.save({
|
|
461
|
+
id: '124',
|
|
462
|
+
customer_id: 'C1',
|
|
463
|
+
items: JSON.stringify([]),
|
|
464
|
+
status: 'shipped',
|
|
465
|
+
total: 50
|
|
466
|
+
});
|
|
467
|
+
await repo.save({
|
|
468
|
+
id: '125',
|
|
469
|
+
customer_id: 'C2',
|
|
470
|
+
items: JSON.stringify([]),
|
|
471
|
+
status: 'pending',
|
|
472
|
+
total: 100
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('returns all orders for customer', async () => {
|
|
477
|
+
const orders = await repo.findByCustomerId('C1');
|
|
478
|
+
|
|
479
|
+
expect(orders).toHaveLength(2);
|
|
480
|
+
expect(orders.map(o => o.id)).toEqual(['123', '124']);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('returns empty array for customer with no orders', async () => {
|
|
484
|
+
const orders = await repo.findByCustomerId('C999');
|
|
485
|
+
|
|
486
|
+
expect(orders).toEqual([]);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('does not return other customers orders', async () => {
|
|
490
|
+
const orders = await repo.findByCustomerId('C1');
|
|
491
|
+
|
|
492
|
+
expect(orders.every(o => o.customer_id === 'C1')).toBe(true);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('findByStatus', () => {
|
|
497
|
+
beforeEach(async () => {
|
|
498
|
+
await repo.save({
|
|
499
|
+
id: '123',
|
|
500
|
+
customer_id: 'C1',
|
|
501
|
+
items: JSON.stringify([]),
|
|
502
|
+
status: 'pending',
|
|
503
|
+
total: 0
|
|
504
|
+
});
|
|
505
|
+
await repo.save({
|
|
506
|
+
id: '124',
|
|
507
|
+
customer_id: 'C1',
|
|
508
|
+
items: JSON.stringify([]),
|
|
509
|
+
status: 'shipped',
|
|
510
|
+
total: 50
|
|
511
|
+
});
|
|
512
|
+
await repo.save({
|
|
513
|
+
id: '125',
|
|
514
|
+
customer_id: 'C2',
|
|
515
|
+
items: JSON.stringify([]),
|
|
516
|
+
status: 'pending',
|
|
517
|
+
total: 100
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('returns orders with given status', async () => {
|
|
522
|
+
const orders = await repo.findByStatus('pending');
|
|
523
|
+
|
|
524
|
+
expect(orders).toHaveLength(2);
|
|
525
|
+
expect(orders.every(o => o.status === 'pending')).toBe(true);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('returns empty array for non-existent status', async () => {
|
|
529
|
+
const orders = await repo.findByStatus('unknown-status');
|
|
530
|
+
|
|
531
|
+
expect(orders).toEqual([]);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
## Consumer Integration Tests
|
|
537
|
+
|
|
538
|
+
Test event message handling.
|
|
539
|
+
|
|
540
|
+
### Consumer Test Setup
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
544
|
+
import { OrderConsumer } from './order-consumer';
|
|
545
|
+
import { OrderService } from './order-service';
|
|
546
|
+
import { Ok, Err } from './result';
|
|
547
|
+
|
|
548
|
+
describe('OrderConsumer', () => {
|
|
549
|
+
let consumer: OrderConsumer;
|
|
550
|
+
let mockService: OrderService;
|
|
551
|
+
|
|
552
|
+
beforeEach(() => {
|
|
553
|
+
mockService = {
|
|
554
|
+
processOrder: vi.fn(),
|
|
555
|
+
handlePayment: vi.fn()
|
|
556
|
+
};
|
|
557
|
+
consumer = new OrderConsumer(mockService);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Event Handling Tests
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
describe('OrderPlaced event', () => {
|
|
566
|
+
it('calls service with order id', async () => {
|
|
567
|
+
mockService.processOrder.mockResolvedValue(Ok({}));
|
|
568
|
+
|
|
569
|
+
const event = {
|
|
570
|
+
type: 'OrderPlaced',
|
|
571
|
+
data: { orderId: '123', customerId: 'C1' }
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
await consumer.handle(event);
|
|
575
|
+
|
|
576
|
+
expect(mockService.processOrder).toHaveBeenCalledWith('123');
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('returns error if service fails', async () => {
|
|
580
|
+
mockService.processOrder.mockRejectedValue(new Error('Process failed'));
|
|
581
|
+
|
|
582
|
+
const event = {
|
|
583
|
+
type: 'OrderPlaced',
|
|
584
|
+
data: { orderId: '123', customerId: 'C1' }
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
expect(async () => {
|
|
588
|
+
await consumer.handle(event);
|
|
589
|
+
}).rejects.toThrow('Process failed');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('handles missing required fields', async () => {
|
|
593
|
+
const event = {
|
|
594
|
+
type: 'OrderPlaced',
|
|
595
|
+
data: { customerId: 'C1' } // Missing orderId
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
expect(async () => {
|
|
599
|
+
await consumer.handle(event);
|
|
600
|
+
}).rejects.toThrow('orderId');
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
describe('PaymentProcessed event', () => {
|
|
605
|
+
it('calls handlePayment with event data', async () => {
|
|
606
|
+
mockService.handlePayment.mockResolvedValue(Ok({}));
|
|
607
|
+
|
|
608
|
+
const event = {
|
|
609
|
+
type: 'PaymentProcessed',
|
|
610
|
+
data: { orderId: '123', amount: 100 }
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
await consumer.handle(event);
|
|
614
|
+
|
|
615
|
+
expect(mockService.handlePayment).toHaveBeenCalledWith({
|
|
616
|
+
orderId: '123',
|
|
617
|
+
amount: 100
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
describe('unknown events', () => {
|
|
623
|
+
it('ignores unknown event types', async () => {
|
|
624
|
+
const event = {
|
|
625
|
+
type: 'UnknownEventType',
|
|
626
|
+
data: {}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
await consumer.handle(event);
|
|
630
|
+
|
|
631
|
+
expect(mockService.processOrder).not.toHaveBeenCalled();
|
|
632
|
+
expect(mockService.handlePayment).not.toHaveBeenCalled();
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Producer Integration Tests
|
|
638
|
+
|
|
639
|
+
Test event publishing.
|
|
640
|
+
|
|
641
|
+
### Producer Test Examples
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
645
|
+
import { OrderProducer } from './order-producer';
|
|
646
|
+
import { MessageQueue } from './message-queue';
|
|
647
|
+
|
|
648
|
+
describe('OrderProducer', () => {
|
|
649
|
+
let producer: OrderProducer;
|
|
650
|
+
let mockQueue: MessageQueue;
|
|
651
|
+
|
|
652
|
+
beforeEach(() => {
|
|
653
|
+
mockQueue = {
|
|
654
|
+
publish: vi.fn()
|
|
655
|
+
};
|
|
656
|
+
producer = new OrderProducer(mockQueue);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
describe('publishOrderCreated', () => {
|
|
660
|
+
it('publishes event with correct schema', async () => {
|
|
661
|
+
mockQueue.publish.mockResolvedValue(undefined);
|
|
662
|
+
|
|
663
|
+
await producer.publishOrderCreated({
|
|
664
|
+
orderId: '123',
|
|
665
|
+
customerId: 'C1',
|
|
666
|
+
total: 100
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(mockQueue.publish).toHaveBeenCalledWith(
|
|
670
|
+
'orders',
|
|
671
|
+
expect.objectContaining({
|
|
672
|
+
type: 'OrderCreated',
|
|
673
|
+
data: expect.objectContaining({
|
|
674
|
+
orderId: '123',
|
|
675
|
+
customerId: 'C1',
|
|
676
|
+
total: 100
|
|
677
|
+
})
|
|
678
|
+
})
|
|
679
|
+
);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('includes timestamp in event', async () => {
|
|
683
|
+
mockQueue.publish.mockResolvedValue(undefined);
|
|
684
|
+
|
|
685
|
+
await producer.publishOrderCreated({
|
|
686
|
+
orderId: '123',
|
|
687
|
+
customerId: 'C1',
|
|
688
|
+
total: 100
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const call = mockQueue.publish.mock.calls[0];
|
|
692
|
+
expect(call[1].timestamp).toBeDefined();
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
## Client Integration Tests
|
|
699
|
+
|
|
700
|
+
Test external API calls with mock server.
|
|
701
|
+
|
|
702
|
+
### Client Test Setup
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
706
|
+
import { setupServer } from 'msw/node';
|
|
707
|
+
import { http, HttpResponse } from 'msw';
|
|
708
|
+
import { PaymentClient } from './payment-client';
|
|
709
|
+
|
|
710
|
+
const mockServer = setupServer();
|
|
711
|
+
|
|
712
|
+
describe('PaymentClient', () => {
|
|
713
|
+
let client: PaymentClient;
|
|
714
|
+
|
|
715
|
+
beforeAll(() => {
|
|
716
|
+
mockServer.listen();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
afterEach(() => {
|
|
720
|
+
mockServer.resetHandlers();
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
afterAll(() => {
|
|
724
|
+
mockServer.close();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
beforeEach(() => {
|
|
728
|
+
client = new PaymentClient('http://payment-api.test');
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### API Call Tests
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
describe('processPayment', () => {
|
|
737
|
+
it('calls payment API with correct payload', async () => {
|
|
738
|
+
mockServer.use(
|
|
739
|
+
http.post('http://payment-api.test/payments', ({ request }) => {
|
|
740
|
+
return HttpResponse.json({ transactionId: 'TXN123' });
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
const result = await client.processPayment({
|
|
745
|
+
orderId: '123',
|
|
746
|
+
amount: 100,
|
|
747
|
+
customerId: 'C1'
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
expect(result.transactionId).toBe('TXN123');
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('handles API errors gracefully', async () => {
|
|
754
|
+
mockServer.use(
|
|
755
|
+
http.post('http://payment-api.test/payments', () => {
|
|
756
|
+
return HttpResponse.json(
|
|
757
|
+
{ error: 'Payment declined' },
|
|
758
|
+
{ status: 400 }
|
|
759
|
+
);
|
|
760
|
+
})
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
expect(async () => {
|
|
764
|
+
await client.processPayment({
|
|
765
|
+
orderId: '123',
|
|
766
|
+
amount: 100,
|
|
767
|
+
customerId: 'C1'
|
|
768
|
+
});
|
|
769
|
+
}).rejects.toThrow('Payment declined');
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('retries on network timeout', async () => {
|
|
773
|
+
let attempts = 0;
|
|
774
|
+
mockServer.use(
|
|
775
|
+
http.post('http://payment-api.test/payments', () => {
|
|
776
|
+
attempts++;
|
|
777
|
+
if (attempts < 2) {
|
|
778
|
+
return HttpResponse.error();
|
|
779
|
+
}
|
|
780
|
+
return HttpResponse.json({ transactionId: 'TXN123' });
|
|
781
|
+
})
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
const result = await client.processPayment({
|
|
785
|
+
orderId: '123',
|
|
786
|
+
amount: 100,
|
|
787
|
+
customerId: 'C1'
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
expect(result.transactionId).toBe('TXN123');
|
|
791
|
+
expect(attempts).toBe(2);
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
```
|