@martinffx/atelier 2.0.0 → 3.0.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 +14 -0
- package/README.md +94 -253
- package/package.json +1 -1
- package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +6 -6
- package/skills/spec-brainstorm/SKILL.md +5 -5
- package/skills/spec-implement/SKILL.md +2 -3
- package/skills/spec-plan/SKILL.md +21 -29
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- package/skills/typescript-testing/references/snapshot-testing.md +0 -774
|
@@ -1,718 +0,0 @@
|
|
|
1
|
-
# Testing Patterns for DynamoDB
|
|
2
|
-
|
|
3
|
-
Comprehensive testing strategies for DynamoDB repositories using DynamoDB Local and Jest.
|
|
4
|
-
|
|
5
|
-
## Test Architecture
|
|
6
|
-
|
|
7
|
-
### Three Testing Layers
|
|
8
|
-
|
|
9
|
-
1. **Repository Tests** - Data access, CRUD, queries, transactions
|
|
10
|
-
2. **Service Tests** - Business logic, orchestration
|
|
11
|
-
3. **Router Tests** - HTTP handling, validation, error responses
|
|
12
|
-
|
|
13
|
-
**Do NOT test:**
|
|
14
|
-
- Entity transformations in isolation (tested through Repository layer)
|
|
15
|
-
- Schema definitions (validated by DynamoDB Toolbox)
|
|
16
|
-
|
|
17
|
-
### Why This Approach?
|
|
18
|
-
|
|
19
|
-
- **Repository tests** verify DynamoDB operations work correctly
|
|
20
|
-
- **Entity transformations** tested implicitly (full round-trip)
|
|
21
|
-
- **Service tests** verify business logic without database overhead
|
|
22
|
-
- **Router tests** verify HTTP layer without business logic
|
|
23
|
-
|
|
24
|
-
## DynamoDB Local Setup
|
|
25
|
-
|
|
26
|
-
### Installation
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
# Using Docker (recommended)
|
|
30
|
-
docker pull amazon/dynamodb-local
|
|
31
|
-
|
|
32
|
-
# Run DynamoDB Local
|
|
33
|
-
docker run -p 8000:8000 amazon/dynamodb-local
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**Or use docker-compose:**
|
|
37
|
-
|
|
38
|
-
```yaml
|
|
39
|
-
# docker-compose.yml
|
|
40
|
-
version: '3.8'
|
|
41
|
-
services:
|
|
42
|
-
dynamodb-local:
|
|
43
|
-
image: amazon/dynamodb-local
|
|
44
|
-
ports:
|
|
45
|
-
- "8000:8000"
|
|
46
|
-
command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-inMemory"]
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Test Configuration
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
// src/test/config.ts
|
|
53
|
-
export const testConfig = {
|
|
54
|
-
database: {
|
|
55
|
-
endpoint: "http://localhost:8000",
|
|
56
|
-
region: "local",
|
|
57
|
-
credentials: {
|
|
58
|
-
accessKeyId: "dummy",
|
|
59
|
-
secretAccessKey: "dummy",
|
|
60
|
-
},
|
|
61
|
-
tableName: "test-github-table",
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Test Setup Pattern
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { DynamoDBClient, CreateTableCommand } from "@aws-sdk/client-dynamodb"
|
|
70
|
-
import { createTableParams, initializeSchema } from "./schema"
|
|
71
|
-
|
|
72
|
-
describe("UserRepository", () => {
|
|
73
|
-
let client: DynamoDBClient
|
|
74
|
-
let userRepo: UserRepository
|
|
75
|
-
|
|
76
|
-
// Unique ID per test run for isolation
|
|
77
|
-
const testRunId = Date.now()
|
|
78
|
-
const testUsername = `testuser-${testRunId}`
|
|
79
|
-
|
|
80
|
-
beforeAll(async () => {
|
|
81
|
-
// 1. Initialize DynamoDB client
|
|
82
|
-
client = new DynamoDBClient({
|
|
83
|
-
endpoint: testConfig.database.endpoint,
|
|
84
|
-
region: testConfig.database.region,
|
|
85
|
-
credentials: testConfig.database.credentials,
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
// 2. Create unique table for this test suite
|
|
89
|
-
const tableName = `test-user-repo-${testRunId}`
|
|
90
|
-
await client.send(new CreateTableCommand(createTableParams(tableName)))
|
|
91
|
-
|
|
92
|
-
// 3. Initialize schema with table name
|
|
93
|
-
const schema = initializeSchema(tableName, client)
|
|
94
|
-
|
|
95
|
-
// 4. Initialize repository
|
|
96
|
-
userRepo = new UserRepository(schema.user)
|
|
97
|
-
}, 15000) // 15s timeout for table creation
|
|
98
|
-
|
|
99
|
-
afterAll(async () => {
|
|
100
|
-
// Cleanup client connection
|
|
101
|
-
client.destroy()
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// Tests...
|
|
105
|
-
})
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
**Key Points:**
|
|
109
|
-
- Use unique table names per test suite (prevents collisions)
|
|
110
|
-
- Use 15s timeout for `beforeAll` (table creation is slow)
|
|
111
|
-
- Clean up client in `afterAll`
|
|
112
|
-
- Don't delete table (in-memory or cleaned by DynamoDB Local restart)
|
|
113
|
-
|
|
114
|
-
## Fixture Factories
|
|
115
|
-
|
|
116
|
-
### Entity Factory Pattern
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
// src/services/entities/fixtures.ts
|
|
120
|
-
|
|
121
|
-
let userCounter = 0
|
|
122
|
-
|
|
123
|
-
export function createUserEntity(
|
|
124
|
-
overrides: Partial<UserEntityOpts> = {}
|
|
125
|
-
): UserEntity {
|
|
126
|
-
const count = ++userCounter
|
|
127
|
-
return new UserEntity({
|
|
128
|
-
username: `testuser${count}`,
|
|
129
|
-
email: `test${count}@example.com`,
|
|
130
|
-
bio: "Test user bio",
|
|
131
|
-
...overrides, // Allow customization
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
let repoCounter = 0
|
|
136
|
-
|
|
137
|
-
export function createRepoEntity(
|
|
138
|
-
overrides: Partial<RepositoryEntityOpts> = {}
|
|
139
|
-
): RepositoryEntity {
|
|
140
|
-
const count = ++repoCounter
|
|
141
|
-
return new RepositoryEntity({
|
|
142
|
-
owner: "testowner",
|
|
143
|
-
repoName: `repo${count}`,
|
|
144
|
-
description: "Test repository",
|
|
145
|
-
isPrivate: false,
|
|
146
|
-
...overrides,
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
**Usage in tests:**
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
it("should create user", async () => {
|
|
155
|
-
const user = createUserEntity({
|
|
156
|
-
username: "alice", // Override username
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
const created = await userRepo.create(user)
|
|
160
|
-
|
|
161
|
-
expect(created.username).toBe("alice")
|
|
162
|
-
expect(created.email).toBe(user.email)
|
|
163
|
-
|
|
164
|
-
// Cleanup
|
|
165
|
-
await userRepo.delete(user.username)
|
|
166
|
-
})
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### Shared DynamoDB Client
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
// src/test/helpers.ts
|
|
173
|
-
|
|
174
|
-
let client: DynamoDBClient | undefined
|
|
175
|
-
|
|
176
|
-
export async function getDDBClient(): Promise<DynamoDBClient> {
|
|
177
|
-
if (!client) {
|
|
178
|
-
client = new DynamoDBClient({
|
|
179
|
-
endpoint: testConfig.database.endpoint,
|
|
180
|
-
region: testConfig.database.region,
|
|
181
|
-
credentials: testConfig.database.credentials,
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
return client
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export async function cleanupDDBClient(): Promise<void> {
|
|
188
|
-
if (client !== undefined) {
|
|
189
|
-
client.destroy()
|
|
190
|
-
client = undefined
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## CRUD Operation Tests
|
|
196
|
-
|
|
197
|
-
### Create Tests
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
describe("create", () => {
|
|
201
|
-
it("should create user successfully", async () => {
|
|
202
|
-
const user = createUserEntity()
|
|
203
|
-
|
|
204
|
-
const created = await userRepo.create(user)
|
|
205
|
-
|
|
206
|
-
expect(created.username).toBe(user.username)
|
|
207
|
-
expect(created.email).toBe(user.email)
|
|
208
|
-
expect(created.created).toBeDefined()
|
|
209
|
-
expect(created.modified).toBeDefined()
|
|
210
|
-
|
|
211
|
-
// Cleanup
|
|
212
|
-
await userRepo.delete(user.username)
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
it("should throw DuplicateEntityError for duplicate username", async () => {
|
|
216
|
-
const user = createUserEntity({ username: "duplicate" })
|
|
217
|
-
|
|
218
|
-
await userRepo.create(user)
|
|
219
|
-
|
|
220
|
-
await expect(userRepo.create(user)).rejects.toThrow(DuplicateEntityError)
|
|
221
|
-
await expect(userRepo.create(user)).rejects.toThrow("already exists")
|
|
222
|
-
|
|
223
|
-
// Cleanup
|
|
224
|
-
await userRepo.delete(user.username)
|
|
225
|
-
})
|
|
226
|
-
})
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Read Tests
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
describe("get", () => {
|
|
233
|
-
it("should return user when exists", async () => {
|
|
234
|
-
const user = createUserEntity()
|
|
235
|
-
await userRepo.create(user)
|
|
236
|
-
|
|
237
|
-
const retrieved = await userRepo.get(user.username)
|
|
238
|
-
|
|
239
|
-
expect(retrieved).toBeDefined()
|
|
240
|
-
expect(retrieved?.username).toBe(user.username)
|
|
241
|
-
|
|
242
|
-
// Cleanup
|
|
243
|
-
await userRepo.delete(user.username)
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it("should return undefined when not found", async () => {
|
|
247
|
-
const retrieved = await userRepo.get("nonexistent")
|
|
248
|
-
|
|
249
|
-
expect(retrieved).toBeUndefined()
|
|
250
|
-
})
|
|
251
|
-
})
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Update Tests
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
describe("update", () => {
|
|
258
|
-
it("should update user successfully", async () => {
|
|
259
|
-
const user = createUserEntity()
|
|
260
|
-
await userRepo.create(user)
|
|
261
|
-
|
|
262
|
-
const updated = user.updateUser({
|
|
263
|
-
email: "newemail@example.com",
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
const result = await userRepo.update(updated)
|
|
267
|
-
|
|
268
|
-
expect(result.email).toBe("newemail@example.com")
|
|
269
|
-
expect(result.username).toBe(user.username)
|
|
270
|
-
|
|
271
|
-
// Cleanup
|
|
272
|
-
await userRepo.delete(user.username)
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
it("should throw EntityNotFoundError when updating non-existent user", async () => {
|
|
276
|
-
const user = createUserEntity({ username: "nonexistent" })
|
|
277
|
-
|
|
278
|
-
await expect(userRepo.update(user)).rejects.toThrow(EntityNotFoundError)
|
|
279
|
-
await expect(userRepo.update(user)).rejects.toThrow("not found")
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Delete Tests
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
describe("delete", () => {
|
|
288
|
-
it("should delete user successfully", async () => {
|
|
289
|
-
const user = createUserEntity()
|
|
290
|
-
await userRepo.create(user)
|
|
291
|
-
|
|
292
|
-
await userRepo.delete(user.username)
|
|
293
|
-
|
|
294
|
-
const retrieved = await userRepo.get(user.username)
|
|
295
|
-
expect(retrieved).toBeUndefined()
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it("should succeed silently when deleting non-existent user", async () => {
|
|
299
|
-
await expect(
|
|
300
|
-
userRepo.delete("nonexistent")
|
|
301
|
-
).resolves.not.toThrow()
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
## Query and Pagination Tests
|
|
307
|
-
|
|
308
|
-
### Basic Query Test
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
describe("listByOwner", () => {
|
|
312
|
-
it("should return all repos for owner", async () => {
|
|
313
|
-
const user = createUserEntity({ username: "alice" })
|
|
314
|
-
await userRepo.create(user)
|
|
315
|
-
|
|
316
|
-
const repo1 = createRepoEntity({ owner: "alice", repoName: "repo1" })
|
|
317
|
-
const repo2 = createRepoEntity({ owner: "alice", repoName: "repo2" })
|
|
318
|
-
|
|
319
|
-
await repoRepo.create(repo1)
|
|
320
|
-
await repoRepo.create(repo2)
|
|
321
|
-
|
|
322
|
-
const result = await repoRepo.listByOwner("alice")
|
|
323
|
-
|
|
324
|
-
expect(result.items).toHaveLength(2)
|
|
325
|
-
expect(result.items.map(r => r.repoName)).toContain("repo1")
|
|
326
|
-
expect(result.items.map(r => r.repoName)).toContain("repo2")
|
|
327
|
-
|
|
328
|
-
// Cleanup
|
|
329
|
-
await repoRepo.delete("alice", "repo1")
|
|
330
|
-
await repoRepo.delete("alice", "repo2")
|
|
331
|
-
await userRepo.delete("alice")
|
|
332
|
-
})
|
|
333
|
-
})
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### Pagination Test
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
describe("pagination", () => {
|
|
340
|
-
it("should paginate results correctly", async () => {
|
|
341
|
-
const user = createUserEntity({ username: "alice" })
|
|
342
|
-
await userRepo.create(user)
|
|
343
|
-
|
|
344
|
-
// Create 5 repos
|
|
345
|
-
for (let i = 1; i <= 5; i++) {
|
|
346
|
-
const repo = createRepoEntity({
|
|
347
|
-
owner: "alice",
|
|
348
|
-
repoName: `repo${i}`,
|
|
349
|
-
})
|
|
350
|
-
await repoRepo.create(repo)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// First page (limit 2)
|
|
354
|
-
const page1 = await repoRepo.listByOwner("alice", 2)
|
|
355
|
-
expect(page1.items).toHaveLength(2)
|
|
356
|
-
expect(page1.offset).toBeDefined()
|
|
357
|
-
|
|
358
|
-
// Second page
|
|
359
|
-
const page2 = await repoRepo.listByOwner("alice", 2, page1.offset)
|
|
360
|
-
expect(page2.items).toHaveLength(2)
|
|
361
|
-
expect(page2.offset).toBeDefined()
|
|
362
|
-
|
|
363
|
-
// Third page (last page)
|
|
364
|
-
const page3 = await repoRepo.listByOwner("alice", 2, page2.offset)
|
|
365
|
-
expect(page3.items).toHaveLength(1)
|
|
366
|
-
expect(page3.offset).toBeUndefined() // No more pages
|
|
367
|
-
|
|
368
|
-
// Cleanup
|
|
369
|
-
for (let i = 1; i <= 5; i++) {
|
|
370
|
-
await repoRepo.delete("alice", `repo${i}`)
|
|
371
|
-
}
|
|
372
|
-
await userRepo.delete("alice")
|
|
373
|
-
})
|
|
374
|
-
})
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### Temporal Sorting Test
|
|
378
|
-
|
|
379
|
-
```typescript
|
|
380
|
-
describe("temporal sorting", () => {
|
|
381
|
-
it("should return repos in newest-first order", async () => {
|
|
382
|
-
const user = createUserEntity({ username: "alice" })
|
|
383
|
-
await userRepo.create(user)
|
|
384
|
-
|
|
385
|
-
// Create repos with delays to ensure different timestamps
|
|
386
|
-
const repo1 = createRepoEntity({ owner: "alice", repoName: "old" })
|
|
387
|
-
await repoRepo.create(repo1)
|
|
388
|
-
|
|
389
|
-
// Wait 100ms
|
|
390
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
391
|
-
|
|
392
|
-
const repo2 = createRepoEntity({ owner: "alice", repoName: "new" })
|
|
393
|
-
await repoRepo.create(repo2)
|
|
394
|
-
|
|
395
|
-
const result = await repoRepo.listByOwner("alice")
|
|
396
|
-
|
|
397
|
-
expect(result.items).toHaveLength(2)
|
|
398
|
-
expect(result.items[0].repoName).toBe("new") // Newest first
|
|
399
|
-
expect(result.items[1].repoName).toBe("old")
|
|
400
|
-
|
|
401
|
-
// Verify timestamps
|
|
402
|
-
expect(result.items[0].modified.toMillis()).toBeGreaterThan(
|
|
403
|
-
result.items[1].modified.toMillis()
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
// Cleanup
|
|
407
|
-
await repoRepo.delete("alice", "old")
|
|
408
|
-
await repoRepo.delete("alice", "new")
|
|
409
|
-
await userRepo.delete("alice")
|
|
410
|
-
})
|
|
411
|
-
})
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
## Concurrency Tests
|
|
415
|
-
|
|
416
|
-
### Atomic Counter Test
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
describe("atomic counter", () => {
|
|
420
|
-
it("should handle concurrent increments without duplicates", async () => {
|
|
421
|
-
const orgId = "testorg"
|
|
422
|
-
const repoId = "testrepo"
|
|
423
|
-
|
|
424
|
-
// Execute 5 increments in parallel
|
|
425
|
-
const increments = Array.from({ length: 5 }, () =>
|
|
426
|
-
counterRepo.incrementAndGet(orgId, repoId)
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
const results = await Promise.all(increments)
|
|
430
|
-
|
|
431
|
-
// All results should be unique
|
|
432
|
-
const uniqueResults = new Set(results)
|
|
433
|
-
expect(uniqueResults.size).toBe(5)
|
|
434
|
-
|
|
435
|
-
// Results should be 1, 2, 3, 4, 5 (order may vary)
|
|
436
|
-
expect(results.sort()).toEqual([1, 2, 3, 4, 5])
|
|
437
|
-
})
|
|
438
|
-
})
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
### Concurrent Issue Creation Test
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
describe("concurrent issue creation", () => {
|
|
445
|
-
it("should assign unique sequential numbers", async () => {
|
|
446
|
-
const user = createUserEntity({ username: "alice" })
|
|
447
|
-
await userRepo.create(user)
|
|
448
|
-
|
|
449
|
-
const repo = createRepoEntity({ owner: "alice", repoName: "test" })
|
|
450
|
-
await repoRepo.create(repo)
|
|
451
|
-
|
|
452
|
-
// Create 5 issues concurrently
|
|
453
|
-
const issues = Array.from({ length: 5 }, (_, i) =>
|
|
454
|
-
IssueEntity.fromRequest({
|
|
455
|
-
owner: "alice",
|
|
456
|
-
repo_name: "test",
|
|
457
|
-
title: `Concurrent Issue ${i}`,
|
|
458
|
-
author: "alice",
|
|
459
|
-
})
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
const created = await Promise.all(
|
|
463
|
-
issues.map(issue => issueRepo.create(issue))
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
// All should have unique numbers
|
|
467
|
-
const numbers = created.map(i => i.issueNumber)
|
|
468
|
-
const uniqueNumbers = new Set(numbers)
|
|
469
|
-
expect(uniqueNumbers.size).toBe(5)
|
|
470
|
-
|
|
471
|
-
// Numbers should be sequential (1, 2, 3, 4, 5)
|
|
472
|
-
expect(numbers.sort()).toEqual([1, 2, 3, 4, 5])
|
|
473
|
-
|
|
474
|
-
// Cleanup
|
|
475
|
-
for (const issue of created) {
|
|
476
|
-
await issueRepo.delete(issue.owner, issue.repoName, issue.issueNumber)
|
|
477
|
-
}
|
|
478
|
-
await repoRepo.delete("alice", "test")
|
|
479
|
-
await userRepo.delete("alice")
|
|
480
|
-
})
|
|
481
|
-
})
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
## Transaction Tests
|
|
485
|
-
|
|
486
|
-
### Parent Validation Test
|
|
487
|
-
|
|
488
|
-
```typescript
|
|
489
|
-
describe("transaction validation", () => {
|
|
490
|
-
it("should throw EntityNotFoundError when creating issue for non-existent repo", async () => {
|
|
491
|
-
const issue = IssueEntity.fromRequest({
|
|
492
|
-
owner: "alice",
|
|
493
|
-
repo_name: "nonexistent",
|
|
494
|
-
title: "Test Issue",
|
|
495
|
-
author: "alice",
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
await expect(issueRepo.create(issue)).rejects.toThrow(EntityNotFoundError)
|
|
499
|
-
await expect(issueRepo.create(issue)).rejects.toThrow("RepositoryEntity")
|
|
500
|
-
})
|
|
501
|
-
|
|
502
|
-
it("should create issue when repo exists", async () => {
|
|
503
|
-
const user = createUserEntity({ username: "alice" })
|
|
504
|
-
await userRepo.create(user)
|
|
505
|
-
|
|
506
|
-
const repo = createRepoEntity({ owner: "alice", repoName: "test" })
|
|
507
|
-
await repoRepo.create(repo)
|
|
508
|
-
|
|
509
|
-
const issue = IssueEntity.fromRequest({
|
|
510
|
-
owner: "alice",
|
|
511
|
-
repo_name: "test",
|
|
512
|
-
title: "Test Issue",
|
|
513
|
-
author: "alice",
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
const created = await issueRepo.create(issue)
|
|
517
|
-
|
|
518
|
-
expect(created.issueNumber).toBe(1)
|
|
519
|
-
expect(created.title).toBe("Test Issue")
|
|
520
|
-
|
|
521
|
-
// Cleanup
|
|
522
|
-
await issueRepo.delete("alice", "test", 1)
|
|
523
|
-
await repoRepo.delete("alice", "test")
|
|
524
|
-
await userRepo.delete("alice")
|
|
525
|
-
})
|
|
526
|
-
})
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
## Test Isolation
|
|
530
|
-
|
|
531
|
-
### Unique Test IDs
|
|
532
|
-
|
|
533
|
-
```typescript
|
|
534
|
-
// Use timestamps for unique IDs
|
|
535
|
-
const testRunId = Date.now()
|
|
536
|
-
const username = `testuser-${testRunId}`
|
|
537
|
-
|
|
538
|
-
// Or use counters
|
|
539
|
-
let testCounter = 0
|
|
540
|
-
const getUniqueUsername = () => `testuser-${++testCounter}`
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Cleanup Pattern
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
describe("UserRepository", () => {
|
|
547
|
-
const createdUsers: string[] = []
|
|
548
|
-
|
|
549
|
-
afterEach(async () => {
|
|
550
|
-
// Clean up all created users after each test
|
|
551
|
-
for (const username of createdUsers) {
|
|
552
|
-
await userRepo.delete(username).catch(() => {}) // Ignore errors
|
|
553
|
-
}
|
|
554
|
-
createdUsers.length = 0 // Clear array
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
it("should create user", async () => {
|
|
558
|
-
const user = createUserEntity()
|
|
559
|
-
createdUsers.push(user.username) // Track for cleanup
|
|
560
|
-
|
|
561
|
-
const created = await userRepo.create(user)
|
|
562
|
-
|
|
563
|
-
expect(created.username).toBe(user.username)
|
|
564
|
-
})
|
|
565
|
-
})
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
## Error Handling Tests
|
|
569
|
-
|
|
570
|
-
### Duplicate Entity Test
|
|
571
|
-
|
|
572
|
-
```typescript
|
|
573
|
-
it("should throw DuplicateEntityError", async () => {
|
|
574
|
-
const user = createUserEntity({ username: "duplicate" })
|
|
575
|
-
|
|
576
|
-
await userRepo.create(user)
|
|
577
|
-
|
|
578
|
-
await expect(userRepo.create(user)).rejects.toThrow(DuplicateEntityError)
|
|
579
|
-
await expect(userRepo.create(user)).rejects.toThrow("already exists")
|
|
580
|
-
await expect(userRepo.create(user)).rejects.toMatchObject({
|
|
581
|
-
entityType: "UserEntity",
|
|
582
|
-
entityKey: "duplicate",
|
|
583
|
-
})
|
|
584
|
-
|
|
585
|
-
await userRepo.delete("duplicate")
|
|
586
|
-
})
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
### Not Found Test
|
|
590
|
-
|
|
591
|
-
```typescript
|
|
592
|
-
it("should throw EntityNotFoundError", async () => {
|
|
593
|
-
const user = createUserEntity({ username: "nonexistent" })
|
|
594
|
-
|
|
595
|
-
await expect(userRepo.update(user)).rejects.toThrow(EntityNotFoundError)
|
|
596
|
-
await expect(userRepo.update(user)).rejects.toThrow("not found")
|
|
597
|
-
})
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
## Test Organization Best Practices
|
|
601
|
-
|
|
602
|
-
### ✓ Do
|
|
603
|
-
|
|
604
|
-
- Use DynamoDB Local for development/CI
|
|
605
|
-
- Create unique table per test suite
|
|
606
|
-
- Use fixture factories for test data
|
|
607
|
-
- Clean up created entities after tests
|
|
608
|
-
- Test error paths explicitly
|
|
609
|
-
- Use unique IDs per test run
|
|
610
|
-
- Test pagination and sorting
|
|
611
|
-
- Test concurrent operations
|
|
612
|
-
- Use 15s timeout for `beforeAll`
|
|
613
|
-
|
|
614
|
-
### ✗ Don't
|
|
615
|
-
|
|
616
|
-
- Don't test entity transformations in isolation
|
|
617
|
-
- Don't skip cleanup (causes test pollution)
|
|
618
|
-
- Don't use shared test data across tests
|
|
619
|
-
- Don't test schema definitions (trust DynamoDB Toolbox)
|
|
620
|
-
- Don't use production DynamoDB for tests
|
|
621
|
-
- Don't rely on test execution order
|
|
622
|
-
|
|
623
|
-
## Testing Checklist
|
|
624
|
-
|
|
625
|
-
For each repository:
|
|
626
|
-
|
|
627
|
-
- [ ] **CRUD tests** - Create, read, update, delete
|
|
628
|
-
- [ ] **Error tests** - Duplicate, not found, validation
|
|
629
|
-
- [ ] **Query tests** - Main table and GSI queries
|
|
630
|
-
- [ ] **Pagination tests** - Multiple pages, last page
|
|
631
|
-
- [ ] **Sorting tests** - Temporal or status-based sorting
|
|
632
|
-
- [ ] **Concurrency tests** - Atomic operations, race conditions
|
|
633
|
-
- [ ] **Transaction tests** - Parent validation, multi-entity
|
|
634
|
-
- [ ] **Cleanup** - Delete all created test data
|
|
635
|
-
- [ ] **Isolation** - Unique IDs, independent tests
|
|
636
|
-
|
|
637
|
-
## Example: Complete Test Suite
|
|
638
|
-
|
|
639
|
-
```typescript
|
|
640
|
-
import { DynamoDBClient, CreateTableCommand } from "@aws-sdk/client-dynamodb"
|
|
641
|
-
import { createTableParams, initializeSchema } from "./schema"
|
|
642
|
-
import { createUserEntity } from "./fixtures"
|
|
643
|
-
|
|
644
|
-
describe("UserRepository", () => {
|
|
645
|
-
let client: DynamoDBClient
|
|
646
|
-
let userRepo: UserRepository
|
|
647
|
-
const testRunId = Date.now()
|
|
648
|
-
const createdUsers: string[] = []
|
|
649
|
-
|
|
650
|
-
beforeAll(async () => {
|
|
651
|
-
client = new DynamoDBClient({
|
|
652
|
-
endpoint: "http://localhost:8000",
|
|
653
|
-
region: "local",
|
|
654
|
-
credentials: { accessKeyId: "dummy", secretAccessKey: "dummy" },
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
const tableName = `test-user-repo-${testRunId}`
|
|
658
|
-
await client.send(new CreateTableCommand(createTableParams(tableName)))
|
|
659
|
-
|
|
660
|
-
const schema = initializeSchema(tableName, client)
|
|
661
|
-
userRepo = new UserRepository(schema.user)
|
|
662
|
-
}, 15000)
|
|
663
|
-
|
|
664
|
-
afterAll(async () => {
|
|
665
|
-
client.destroy()
|
|
666
|
-
})
|
|
667
|
-
|
|
668
|
-
afterEach(async () => {
|
|
669
|
-
for (const username of createdUsers) {
|
|
670
|
-
await userRepo.delete(username).catch(() => {})
|
|
671
|
-
}
|
|
672
|
-
createdUsers.length = 0
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
describe("create", () => {
|
|
676
|
-
it("should create user successfully", async () => {
|
|
677
|
-
const user = createUserEntity()
|
|
678
|
-
createdUsers.push(user.username)
|
|
679
|
-
|
|
680
|
-
const created = await userRepo.create(user)
|
|
681
|
-
|
|
682
|
-
expect(created.username).toBe(user.username)
|
|
683
|
-
expect(created.email).toBe(user.email)
|
|
684
|
-
})
|
|
685
|
-
|
|
686
|
-
it("should throw DuplicateEntityError", async () => {
|
|
687
|
-
const user = createUserEntity({ username: "duplicate" })
|
|
688
|
-
createdUsers.push(user.username)
|
|
689
|
-
|
|
690
|
-
await userRepo.create(user)
|
|
691
|
-
|
|
692
|
-
await expect(userRepo.create(user)).rejects.toThrow(DuplicateEntityError)
|
|
693
|
-
})
|
|
694
|
-
})
|
|
695
|
-
|
|
696
|
-
describe("get", () => {
|
|
697
|
-
it("should return user when exists", async () => {
|
|
698
|
-
const user = createUserEntity()
|
|
699
|
-
createdUsers.push(user.username)
|
|
700
|
-
|
|
701
|
-
await userRepo.create(user)
|
|
702
|
-
|
|
703
|
-
const retrieved = await userRepo.get(user.username)
|
|
704
|
-
|
|
705
|
-
expect(retrieved).toBeDefined()
|
|
706
|
-
expect(retrieved?.username).toBe(user.username)
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
it("should return undefined when not found", async () => {
|
|
710
|
-
const retrieved = await userRepo.get("nonexistent")
|
|
711
|
-
|
|
712
|
-
expect(retrieved).toBeUndefined()
|
|
713
|
-
})
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
// ... more tests
|
|
717
|
-
})
|
|
718
|
-
```
|