sonamu 0.10.5 → 0.10.7
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/dist/api/base-frame.d.ts +2 -0
- package/dist/api/base-frame.d.ts.map +1 -1
- package/dist/api/base-frame.js +8 -1
- package/dist/api/decorators.js +1 -1
- package/dist/api/sonamu.js +1 -1
- package/dist/bin/cli.js +195 -187
- package/dist/cone/cone-generator.js +3 -9
- package/dist/database/base-model.js +2 -2
- package/dist/database/knex.d.ts.map +1 -1
- package/dist/database/knex.js +2 -2
- package/dist/database/puri.d.ts +17 -0
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +102 -3
- package/dist/entity/entity-manager.js +1 -1
- package/dist/index.js +3 -3
- package/dist/migration/code-generation.js +6 -6
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +52 -7
- package/dist/migration/slack-confirm.js +1 -1
- package/dist/naite/naite-reporter.js +1 -1
- package/dist/syncer/checksum.js +1 -1
- package/dist/syncer/code-generator.js +1 -1
- package/dist/syncer/entity-operations.js +1 -1
- package/dist/syncer/syncer-actions.js +1 -1
- package/dist/syncer/syncer.js +1 -1
- package/dist/ui-web/assets/{index-BLX4zN8c.js → index-D0MHYbxl.js} +1 -1
- package/dist/ui-web/assets/index-Dx_JX4aQ.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/dist/utils/formatter.js +1 -1
- package/package.json +3 -3
- package/src/api/base-frame.ts +14 -1
- package/src/bin/cli.ts +268 -280
- package/src/cone/cone-generator.ts +2 -18
- package/src/database/__tests__/puri.test.ts +183 -0
- package/src/database/knex.ts +4 -1
- package/src/database/puri.ts +229 -2
- package/src/database/puri.types.test-d.ts +56 -0
- package/src/migration/code-generation.ts +5 -5
- package/src/migration/migrator.ts +81 -7
- package/src/skills/AGENTS.md +76 -50
- package/src/skills/sonamu/SKILL.md +46 -227
- package/src/skills/{sonamu/ai-agents.md → sonamu-ai-agents/SKILL.md} +3 -3
- package/src/skills/{sonamu/api.md → sonamu-api/SKILL.md} +3 -2
- package/src/skills/{sonamu/auth.md → sonamu-auth/SKILL.md} +9 -4
- package/src/skills/{sonamu/auth-plugins.md → sonamu-auth/references/plugins.md} +2 -7
- package/src/skills/sonamu-auth/references/user-id-migration-followups.md +192 -0
- package/src/skills/{sonamu/auth-migration.md → sonamu-auth/references/user-id-migration.md} +1 -195
- package/src/skills/sonamu-config/SKILL.md +203 -0
- package/src/skills/{sonamu → sonamu-config/references}/database.md +3 -8
- package/src/skills/sonamu-config/references/environments.md +178 -0
- package/src/skills/sonamu-config/references/server-options.md +400 -0
- package/src/skills/sonamu-entity/SKILL.md +180 -0
- package/src/skills/{sonamu/entity-validation-checklist.md → sonamu-entity/references/creation-workflow.md} +117 -8
- package/src/skills/sonamu-entity/references/design-guides.md +243 -0
- package/src/skills/sonamu-entity/references/field-types.md +170 -0
- package/src/skills/sonamu-entity/references/relations-detail.md +245 -0
- package/src/skills/{sonamu/entity-relations.md → sonamu-entity/references/relations.md} +1 -258
- package/src/skills/{sonamu → sonamu-entity/references}/subset.md +1 -11
- package/src/skills/sonamu-fixture/SKILL.md +180 -0
- package/src/skills/{sonamu/fixture-cli.md → sonamu-fixture/references/cli-usage.md} +5 -176
- package/src/skills/{sonamu → sonamu-fixture/references}/cone.md +4 -14
- package/src/skills/sonamu-frontend/SKILL.md +142 -0
- package/src/skills/sonamu-frontend/references/components.md +323 -0
- package/src/skills/sonamu-frontend/references/examples.md +64 -0
- package/src/skills/sonamu-frontend/references/hooks.md +273 -0
- package/src/skills/sonamu-frontend/references/runtime.md +165 -0
- package/src/skills/{sonamu → sonamu-frontend/references}/scaffolding.md +3 -8
- package/src/skills/{sonamu/i18n.md → sonamu-i18n/SKILL.md} +2 -2
- package/src/skills/{sonamu/migration.md → sonamu-migration/SKILL.md} +1 -1
- package/src/skills/{sonamu/naite.md → sonamu-naite/SKILL.md} +5 -5
- package/src/skills/sonamu-query/SKILL.md +48 -0
- package/src/skills/sonamu-query/references/model-patterns.md +390 -0
- package/src/skills/{sonamu → sonamu-query/references}/model.md +1 -401
- package/src/skills/{sonamu → sonamu-query/references}/puri.md +31 -243
- package/src/skills/sonamu-query/references/search.md +238 -0
- package/src/skills/{sonamu → sonamu-query/references}/upsert.md +1 -6
- package/src/skills/{sonamu/tasks.md → sonamu-tasks/SKILL.md} +1 -1
- package/src/skills/sonamu-testing/SKILL.md +251 -0
- package/src/skills/{sonamu/testing-devrunner.md → sonamu-testing/references/devrunner.md} +4 -9
- package/src/skills/sonamu-testing/references/helpers.md +185 -0
- package/src/skills/sonamu-testing/references/patterns.md +263 -0
- package/src/skills/sonamu-testing/references/pitfalls.md +588 -0
- package/src/skills/sonamu-testing/references/quick-start.md +285 -0
- package/src/skills/sonamu-testing/references/type-safety.md +172 -0
- package/src/skills/sonamu-testing/references/writing-plan.md +375 -0
- package/src/skills/{sonamu/vector.md → sonamu-vector/SKILL.md} +1 -1
- package/dist/ui-web/assets/index-Dx4ap5i4.css +0 -1
- package/src/skills/commands/sonamu-skills.md +0 -20
- package/src/skills/project/README.md +0 -19
- package/src/skills/project/architecture.md +0 -373
- package/src/skills/sonamu/cdd.md +0 -129
- package/src/skills/sonamu/config.md +0 -772
- package/src/skills/sonamu/create-sonamu.md +0 -208
- package/src/skills/sonamu/entity-basic.md +0 -678
- package/src/skills/sonamu/framework-change.md +0 -96
- package/src/skills/sonamu/frontend.md +0 -915
- package/src/skills/sonamu/project-init.md +0 -477
- package/src/skills/sonamu/skill-contribution.md +0 -247
- package/src/skills/sonamu/testing.md +0 -2163
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Test Helpers
|
|
2
|
+
|
|
3
|
+
## Fixture
|
|
4
|
+
|
|
5
|
+
### createFixtureLoader
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// api/src/testing/fixture.ts
|
|
9
|
+
import { createFixtureLoader } from "sonamu/test";
|
|
10
|
+
import { CompanyModel } from "../application/company/company.model";
|
|
11
|
+
import { UserModel } from "../application/user/user.model";
|
|
12
|
+
|
|
13
|
+
export const loadFixtures = createFixtureLoader({
|
|
14
|
+
company01: async () => CompanyModel.findById("A", 1),
|
|
15
|
+
user01: async () => UserModel.findById("A", 1),
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Using in tests
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { loadFixtures } from "../../testing/fixture";
|
|
23
|
+
|
|
24
|
+
test("update company info", async () => {
|
|
25
|
+
const f0 = await loadFixtures(["company01"]);
|
|
26
|
+
|
|
27
|
+
await CompanyModel.save([
|
|
28
|
+
{
|
|
29
|
+
...f0.company01,
|
|
30
|
+
name: "Updated Company",
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const f1 = await loadFixtures(["company01"]);
|
|
35
|
+
expect(f1.company01.name).toBe("Updated Company");
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Naite (Test Tracing System)
|
|
40
|
+
|
|
41
|
+
**→ Detailed guide (key list, chaining filters, wildcard, del, internal structure): `sonamu-naite`**
|
|
42
|
+
|
|
43
|
+
Naite is a tracing system that records values with `Naite.t("key", value)` in source code and validates them with `Naite.get("key")` in tests.
|
|
44
|
+
|
|
45
|
+
### Commonly Used Patterns in Tests
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { Naite } from "sonamu";
|
|
49
|
+
|
|
50
|
+
// Query validation
|
|
51
|
+
expect(Naite.get("esq-query").first()).not.contain("limit");
|
|
52
|
+
|
|
53
|
+
// UpsertBuilder behavior validation
|
|
54
|
+
const trace = Naite.get("puri:ub-upserted").first();
|
|
55
|
+
expect(trace).toMatchObject({ tableName: "users", rowCount: 3 });
|
|
56
|
+
|
|
57
|
+
// Fetch methods: .first(), .last(), .at(n), .result() (full array)
|
|
58
|
+
// Filters: .fromFile("user.model.ts"), .fromFunction("findById"), .where("data.tableName", "=", "users")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Test Helper: expectQuery
|
|
63
|
+
|
|
64
|
+
Helper for validating specific parts of SQL queries (see miomock for reference):
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// api/src/testing/expect-query.ts
|
|
68
|
+
import { type AST, Parser } from "node-sql-parser";
|
|
69
|
+
import { expect } from "vitest";
|
|
70
|
+
|
|
71
|
+
export type QueryPart =
|
|
72
|
+
| "type"
|
|
73
|
+
| "table"
|
|
74
|
+
| "columns"
|
|
75
|
+
| "set"
|
|
76
|
+
| "where"
|
|
77
|
+
| "join"
|
|
78
|
+
| "orderBy"
|
|
79
|
+
| "pagination"
|
|
80
|
+
| "groupBy"
|
|
81
|
+
| "having";
|
|
82
|
+
|
|
83
|
+
export function expectQuery(query: string, part?: QueryPart) {
|
|
84
|
+
if (!part) return expect(query);
|
|
85
|
+
const ast = parseQuery(query);
|
|
86
|
+
const extractedSql = extractors[part](ast);
|
|
87
|
+
return expect(extractedSql);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Usage Examples
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { expectQuery } from "../testing/expect-query";
|
|
95
|
+
|
|
96
|
+
test("validate select query", async () => {
|
|
97
|
+
const db = UserModel.getPuri("r");
|
|
98
|
+
await db.table("users").select({ id: "users.id" });
|
|
99
|
+
const query = Naite.get("puri:executed-query").first();
|
|
100
|
+
|
|
101
|
+
expectQuery(query, "type").toBe("select");
|
|
102
|
+
expectQuery(query, "table").toBe("users");
|
|
103
|
+
expectQuery(query, "columns").toMatchInlineSnapshot(`""users"."id" AS \`id\`"`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("validate where condition", async () => {
|
|
107
|
+
const db = UserModel.getPuri("r");
|
|
108
|
+
await db.table("users").where("users.id", 1);
|
|
109
|
+
const query = Naite.get("puri:executed-query").first();
|
|
110
|
+
|
|
111
|
+
expectQuery(query, "where").toMatchInlineSnapshot(`""users"."id" = 1"`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("validate join", async () => {
|
|
115
|
+
const db = UserModel.getPuri("r");
|
|
116
|
+
await db.table("employees").leftJoin("departments", "employees.department_id", "departments.id");
|
|
117
|
+
const query = Naite.get("puri:executed-query").first();
|
|
118
|
+
|
|
119
|
+
expectQuery(query, "join").toMatchInlineSnapshot(
|
|
120
|
+
`"LEFT JOIN departments ON "employees"."department_id" = "departments"."id""`,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Test Helper: expectUB
|
|
126
|
+
|
|
127
|
+
UpsertBuilder state validation helper (see miomock for reference):
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// api/src/testing/expect-ub.ts
|
|
131
|
+
import type { UpsertBuilder } from "sonamu";
|
|
132
|
+
import { expect } from "vitest";
|
|
133
|
+
|
|
134
|
+
export type UBPart =
|
|
135
|
+
| "tables"
|
|
136
|
+
| "hasTable"
|
|
137
|
+
| "rowCount"
|
|
138
|
+
| "rows"
|
|
139
|
+
| "row"
|
|
140
|
+
| "refs"
|
|
141
|
+
| "uniquesMap"
|
|
142
|
+
| "uniqueIndexes";
|
|
143
|
+
|
|
144
|
+
export function expectUB<P extends UBPart>(
|
|
145
|
+
ub: UpsertBuilder,
|
|
146
|
+
part: P,
|
|
147
|
+
tableName?: string,
|
|
148
|
+
index?: number,
|
|
149
|
+
) {
|
|
150
|
+
// ... implementation
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Usage Examples
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { expectUB } from "../testing/expect-ub";
|
|
158
|
+
|
|
159
|
+
test("validate UpsertBuilder state", async () => {
|
|
160
|
+
const ub = new UpsertBuilder();
|
|
161
|
+
|
|
162
|
+
// initial state
|
|
163
|
+
expectUB(ub, "hasTable", "users").toBe(false);
|
|
164
|
+
expectUB(ub, "tables").toEqual([]);
|
|
165
|
+
|
|
166
|
+
// after register
|
|
167
|
+
ub.register("users", {
|
|
168
|
+
email: "test@test.com",
|
|
169
|
+
username: "test",
|
|
170
|
+
password: "pw",
|
|
171
|
+
role: "normal",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expectUB(ub, "hasTable", "users").toBe(true);
|
|
175
|
+
expectUB(ub, "rowCount", "users").toBe(1);
|
|
176
|
+
expectUB(ub, "row", "users", 0).toMatchObject({
|
|
177
|
+
email: "test@test.com",
|
|
178
|
+
username: "test",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// confirm reset after upsert
|
|
182
|
+
await ub.upsert(wdb, "users");
|
|
183
|
+
expectUB(ub, "rowCount", "users").toBe(0);
|
|
184
|
+
});
|
|
185
|
+
```
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Test Patterns
|
|
2
|
+
|
|
3
|
+
## Enum Value Usage Rules
|
|
4
|
+
|
|
5
|
+
**CRITICAL: Only use enum values defined in entity.json.**
|
|
6
|
+
|
|
7
|
+
### Rules
|
|
8
|
+
|
|
9
|
+
1. Check the exact value list for enum fields in entity.json
|
|
10
|
+
2. If possible, use TypeScript enum types from `sonamu.generated.ts` (type-safe)
|
|
11
|
+
3. Set valid enum values as defaults in test-helpers.ts
|
|
12
|
+
4. Do not use arbitrary strings
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// WRONG: written based on guesses
|
|
16
|
+
role: "user"; // entity.json defines it as "normal"
|
|
17
|
+
status: "in_progress"; // entity.json defines it as "pending"
|
|
18
|
+
|
|
19
|
+
// CORRECT: written after checking entity.json
|
|
20
|
+
role: "normal"; // exact value from entity.json
|
|
21
|
+
status: "pending"; // exact value from entity.json
|
|
22
|
+
|
|
23
|
+
// BEST: use TypeScript enum
|
|
24
|
+
import { UserRoleEnum } from "../sonamu.generated";
|
|
25
|
+
role: UserRoleEnum.normal;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Core principle: entity.json is the Single Source of Truth.**
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Test Basic Patterns
|
|
32
|
+
|
|
33
|
+
### bootstrap
|
|
34
|
+
|
|
35
|
+
`bootstrap(vi)` call required in all test files:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { bootstrap, test } from "sonamu/test";
|
|
39
|
+
import { describe, expect, vi } from "vitest";
|
|
40
|
+
|
|
41
|
+
bootstrap(vi);
|
|
42
|
+
|
|
43
|
+
describe("MyTest", () => {
|
|
44
|
+
test("test case", async () => {
|
|
45
|
+
// ...
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**bootstrap options:**
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Default: forTesting: true (fast, skips Syncer/Task)
|
|
54
|
+
bootstrap(vi);
|
|
55
|
+
|
|
56
|
+
// forTesting: false - full initialization (loads Syncer, Task, EntityManager, etc.)
|
|
57
|
+
// Used in tests for migrator, syncer, template, etc.
|
|
58
|
+
bootstrap(vi, { forTesting: false });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### test vs testAs
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Unauthenticated test - Context.user is null
|
|
65
|
+
test("unauthenticated test", async () => {
|
|
66
|
+
const me = await UserModel.me();
|
|
67
|
+
expect(me).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Authenticated test - Context.user is set
|
|
71
|
+
import type { UserSubsetSS } from "../sonamu.generated";
|
|
72
|
+
|
|
73
|
+
const adminUser: UserSubsetSS = {
|
|
74
|
+
id: 1,
|
|
75
|
+
created_at: new Date(),
|
|
76
|
+
email: "admin@test.com",
|
|
77
|
+
username: "admin",
|
|
78
|
+
role: "admin",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
testAs(adminUser, "admin permission test", async () => {
|
|
82
|
+
const me = await UserModel.me();
|
|
83
|
+
expect(me?.role).toBe("admin");
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### test.each
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
test.each([
|
|
91
|
+
{ input: "user@example.com", expected: true },
|
|
92
|
+
{ input: "invalid-email", expected: false },
|
|
93
|
+
])("email validation: $input → $expected", async ({ input, expected }) => {
|
|
94
|
+
expect(validateEmail(input)).toBe(expected);
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Mock Patterns
|
|
100
|
+
|
|
101
|
+
### setup-mocks.ts
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// api/src/testing/setup-mocks.ts
|
|
105
|
+
import { Naite } from "sonamu";
|
|
106
|
+
import { vi } from "vitest";
|
|
107
|
+
|
|
108
|
+
vi.mock("fs/promises", async (importOriginal) => {
|
|
109
|
+
const actual = (await importOriginal()) as typeof import("fs/promises");
|
|
110
|
+
return {
|
|
111
|
+
...actual,
|
|
112
|
+
access: vi.fn((path, mode) => {
|
|
113
|
+
// virtual file system check
|
|
114
|
+
const vfs = Naite.get("mock:fs/promises:virtualFileSystem").result();
|
|
115
|
+
if (vfs.some((v) => v === path)) {
|
|
116
|
+
return Promise.resolve();
|
|
117
|
+
}
|
|
118
|
+
return actual.access(path, mode);
|
|
119
|
+
}),
|
|
120
|
+
writeFile: vi.fn((path, data) => {
|
|
121
|
+
Naite.t("fs/promises:writeFile", { path, data });
|
|
122
|
+
}),
|
|
123
|
+
rm: vi.fn(async (path, options) => {
|
|
124
|
+
Naite.t("fs/promises:rm", { path, options });
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### test-helpers.ts
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// api/src/testing/test-helpers.ts
|
|
135
|
+
import { Entity, EntityManager, type EntityJson } from "sonamu";
|
|
136
|
+
import { vi } from "vitest";
|
|
137
|
+
|
|
138
|
+
// Mocking EntityManager.get
|
|
139
|
+
export function mockEntityManagerGet(
|
|
140
|
+
targetEntityId: string,
|
|
141
|
+
overrideCallback: (original: EntityJson) => EntityJson,
|
|
142
|
+
) {
|
|
143
|
+
const originalEntityJson = EntityManager.get(targetEntityId).toJson();
|
|
144
|
+
const originalGet = EntityManager.get;
|
|
145
|
+
return vi.spyOn(EntityManager, "get").mockImplementation((entityId) => {
|
|
146
|
+
if (entityId === targetEntityId) {
|
|
147
|
+
return new Entity(overrideCallback(originalEntityJson));
|
|
148
|
+
}
|
|
149
|
+
return originalGet.call(EntityManager, entityId);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## CRUD Test Patterns
|
|
155
|
+
|
|
156
|
+
### Create & Read
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
test("Create - create new user", async () => {
|
|
160
|
+
const [userId] = await UserModel.save([
|
|
161
|
+
{
|
|
162
|
+
email: "newuser@test.com",
|
|
163
|
+
username: "newuser",
|
|
164
|
+
password: "hashedpassword",
|
|
165
|
+
role: "normal",
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
expect(userId).toBeGreaterThan(0);
|
|
170
|
+
|
|
171
|
+
const user = await UserModel.findById("A", userId);
|
|
172
|
+
expect(user.email).toBe("newuser@test.com");
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Update
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
test("Update - update user", async () => {
|
|
180
|
+
const f0 = await loadFixtures(["user01"]);
|
|
181
|
+
|
|
182
|
+
await UserModel.save([
|
|
183
|
+
{
|
|
184
|
+
...f0.user01,
|
|
185
|
+
username: "updated_username",
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
const f1 = await loadFixtures(["user01"]);
|
|
190
|
+
expect(f1.user01.username).toBe("updated_username");
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Error Tests
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
test("error when fetching non-existent user", async () => {
|
|
198
|
+
await expect(UserModel.findById("A", 99999)).rejects.toThrow("not found");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("unresolved reference error", async () => {
|
|
202
|
+
const ub = new UpsertBuilder();
|
|
203
|
+
const companyRef = ub.register("companies", { name: "Test" });
|
|
204
|
+
ub.register("departments", { company_id: companyRef, name: "Dept" });
|
|
205
|
+
|
|
206
|
+
// attempt upsert in wrong order
|
|
207
|
+
await expect(ub.upsert(wdb, "departments")).rejects.toThrow(/unresolved reference/);
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Test Structuring Patterns
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
describe("UpsertBuilder", () => {
|
|
215
|
+
describe("A. Basic registration (register)", () => {
|
|
216
|
+
test("register() returns UBRef", async () => {
|
|
217
|
+
/* ... */
|
|
218
|
+
});
|
|
219
|
+
test("multiple register() calls accumulate rows", async () => {
|
|
220
|
+
/* ... */
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("B. Table management", () => {
|
|
225
|
+
test("basic behavior of getTable()/hasTable()", async () => {
|
|
226
|
+
/* ... */
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("C. Upsert execution", () => {
|
|
231
|
+
test("upsert() - insert new row", async () => {
|
|
232
|
+
/* ... */
|
|
233
|
+
});
|
|
234
|
+
test("upsert() - update existing row", async () => {
|
|
235
|
+
/* ... */
|
|
236
|
+
});
|
|
237
|
+
test("insertOnly() - insert only", async () => {
|
|
238
|
+
/* ... */
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("D. Error handling", () => {
|
|
243
|
+
test("upsert on non-existent table → empty array", async () => {
|
|
244
|
+
/* ... */
|
|
245
|
+
});
|
|
246
|
+
test("unresolved reference → error", async () => {
|
|
247
|
+
/* ... */
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## File Structure
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
api/src/testing/
|
|
257
|
+
├── fixture.ts # createFixtureLoader definition
|
|
258
|
+
├── global.ts # globalSetup (dotenv, setup export)
|
|
259
|
+
├── setup-mocks.ts # global Mock configuration
|
|
260
|
+
├── test-helpers.ts # test utility functions
|
|
261
|
+
├── expect-query.ts # SQL query validation helper
|
|
262
|
+
└── expect-ub.ts # UpsertBuilder validation helper
|
|
263
|
+
```
|