interaqt 0.3.0 → 0.3.1
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/agent/.claude/agents/code-generation-handler.md +2 -0
- package/agent/.claude/agents/computation-generation-handler.md +1 -0
- package/agent/.claude/agents/implement-design-handler.md +4 -13
- package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
- package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
- package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
- package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
- package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
- package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
- package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
- package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
- package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
- package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
- package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
- package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
- package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
- package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
- package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
- package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
- package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
- package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
- package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
- package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
- package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
- package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
- package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
- package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
- package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
- package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
- package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
- package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
- package/agent/agentspace/knowledge/usage/README.md +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
In the interaqt framework, testing is part of the design philosophy. The reactive programming model makes testing more intuitive and reliable. This chapter will detail testing strategies, patterns, and best practices.
|
|
4
|
+
|
|
5
|
+
## Testing API Quick Reference
|
|
6
|
+
|
|
7
|
+
### ⚠️ Common API Mistakes to Avoid
|
|
8
|
+
|
|
9
|
+
Many LLMs generate incorrect API usage. Here's the correct way to use interaqt testing APIs:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// ❌ WRONG: These APIs do NOT exist
|
|
13
|
+
controller.run() // ❌ No such method
|
|
14
|
+
storage.findByProperty('Entity', 'prop') // ❌ No such method
|
|
15
|
+
controller.execute() // ❌ No such method
|
|
16
|
+
controller.dispatch() // ❌ No such method
|
|
17
|
+
|
|
18
|
+
// ✅ CORRECT: Use these APIs instead
|
|
19
|
+
controller.callInteraction('InteractionName', args) // ✅ Call interactions
|
|
20
|
+
storage.findOne('Entity', MatchExp) // ✅ Find single record
|
|
21
|
+
storage.find('Entity', MatchExp) // ✅ Find multiple records
|
|
22
|
+
storage.create('Entity', data) // ✅ Create record
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Complete Test Template
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { describe, test, expect, beforeEach } from 'vitest'
|
|
29
|
+
import { Controller, MonoSystem, KlassByName, PGLiteDB, MatchExp } from 'interaqt'
|
|
30
|
+
import { entities, relations, interactions, activities } from '../backend'
|
|
31
|
+
// If you need UUID, install and import it:
|
|
32
|
+
// npm install uuid @types/uuid
|
|
33
|
+
// import { v4 as uuid } from 'uuid'
|
|
34
|
+
|
|
35
|
+
describe('Feature Tests', () => {
|
|
36
|
+
let system: MonoSystem
|
|
37
|
+
let controller: Controller
|
|
38
|
+
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
// ✅ Correct setup
|
|
41
|
+
system = new MonoSystem(new PGLiteDB())
|
|
42
|
+
system.conceptClass = KlassByName
|
|
43
|
+
|
|
44
|
+
// Note: When creating relations, DO NOT specify name property
|
|
45
|
+
// The framework automatically generates relation names:
|
|
46
|
+
// - User + Post → UserPost
|
|
47
|
+
// - Post + Comment → PostComment
|
|
48
|
+
|
|
49
|
+
controller = new Controller({
|
|
50
|
+
system,
|
|
51
|
+
entities,
|
|
52
|
+
relations, // Relations with auto-generated names
|
|
53
|
+
activities, // Activities
|
|
54
|
+
interactions, // Interactions
|
|
55
|
+
dict: [], // Global dictionaries (NOT computations)
|
|
56
|
+
recordMutationSideEffects: [] // Side effects
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await controller.setup(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('interaction test example', async () => {
|
|
63
|
+
// ✅ CORRECT: Use callInteraction
|
|
64
|
+
const result = await controller.callInteraction('CreateUser', {
|
|
65
|
+
user: { id: 'system', role: 'admin' }, // Must include user object
|
|
66
|
+
payload: {
|
|
67
|
+
username: 'testuser',
|
|
68
|
+
email: 'test@example.com',
|
|
69
|
+
role: 'user'
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Check for errors
|
|
74
|
+
expect(result.error).toBeUndefined()
|
|
75
|
+
|
|
76
|
+
// ✅ CORRECT: Use storage.findOne with MatchExp
|
|
77
|
+
const user = await system.storage.findOne(
|
|
78
|
+
'User',
|
|
79
|
+
MatchExp.atom({ key: 'username', value: ['=', 'testuser'] }),
|
|
80
|
+
undefined,
|
|
81
|
+
['id', 'username', 'email', 'role', 'status']
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect(user).toBeTruthy()
|
|
85
|
+
expect(user.email).toBe('test@example.com')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('finding records examples', async () => {
|
|
89
|
+
// ✅ Find by single field
|
|
90
|
+
const user = await system.storage.findOne(
|
|
91
|
+
'User',
|
|
92
|
+
MatchExp.atom({ key: 'id', value: ['=', userId] }),
|
|
93
|
+
undefined,
|
|
94
|
+
['id', 'username', 'email', 'status', 'role']
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// ✅ Find with multiple conditions
|
|
98
|
+
const activeUsers = await system.storage.find(
|
|
99
|
+
'User',
|
|
100
|
+
MatchExp.atom({ key: 'status', value: ['=', 'active'] })
|
|
101
|
+
.and({ key: 'role', value: ['=', 'user'] }),
|
|
102
|
+
undefined,
|
|
103
|
+
['id', 'username', 'email', 'lastLoginDate']
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// ✅ Find with complex conditions
|
|
107
|
+
const posts = await system.storage.find(
|
|
108
|
+
'Post',
|
|
109
|
+
MatchExp.atom({ key: 'author.id', value: ['=', userId] })
|
|
110
|
+
.and({ key: 'status', value: ['in', ['published', 'draft']] }),
|
|
111
|
+
undefined,
|
|
112
|
+
['id', 'title', 'content', 'status', 'createdAt', 'author']
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('creating and updating records', async () => {
|
|
117
|
+
// ✅ Create record directly (ONLY for test setup)
|
|
118
|
+
// ⚠️ WARNING: storage.create bypasses ALL validation!
|
|
119
|
+
// NEVER use it to test business logic or validation
|
|
120
|
+
const user = await system.storage.create('User', {
|
|
121
|
+
username: 'testuser',
|
|
122
|
+
email: 'test@example.com',
|
|
123
|
+
role: 'user'
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// ✅ Update record (also bypasses validation - use only for test setup)
|
|
127
|
+
await system.storage.update(
|
|
128
|
+
'User',
|
|
129
|
+
MatchExp.atom({ key: 'id', value: ['=', user.id] }),
|
|
130
|
+
{ status: 'active' }
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// ✅ Delete record
|
|
134
|
+
await system.storage.delete(
|
|
135
|
+
'User',
|
|
136
|
+
MatchExp.atom({ key: 'id', value: ['=', user.id] })
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Key API Methods
|
|
143
|
+
|
|
144
|
+
#### 1. Controller APIs
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Call an interaction (the ONLY way to execute business logic)
|
|
148
|
+
const result = await controller.callInteraction(interactionName: string, args: {
|
|
149
|
+
user: { id: string, [key: string]: any }, // Required user object
|
|
150
|
+
payload?: { [key: string]: any } // Optional payload
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Call activity interaction
|
|
154
|
+
const result = await controller.callActivityInteraction(
|
|
155
|
+
activityName: string,
|
|
156
|
+
interactionName: string,
|
|
157
|
+
activityId: string,
|
|
158
|
+
args: InteractionEventArgs
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### 2. Storage APIs
|
|
163
|
+
|
|
164
|
+
⚠️ **WARNING: Storage APIs bypass ALL validation and business logic!**
|
|
165
|
+
- Use `storage.create/update/delete` ONLY for test data setup
|
|
166
|
+
- NEVER use them to test validation or business logic
|
|
167
|
+
- ALL business logic tests must use `callInteraction`
|
|
168
|
+
|
|
169
|
+
🔴 **CRITICAL: Always specify attributeQuery when using find/findOne!**
|
|
170
|
+
- Without `attributeQuery`, only the `id` field is returned
|
|
171
|
+
- This is the most common cause of test failures
|
|
172
|
+
- Always explicitly list all fields you need to verify
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Create a record (ONLY for test setup - bypasses ALL validation!)
|
|
176
|
+
const record = await system.storage.create(entityName: string, data: object)
|
|
177
|
+
|
|
178
|
+
// Find one record (safe for reading data)
|
|
179
|
+
const record = await system.storage.findOne(
|
|
180
|
+
entityName: string,
|
|
181
|
+
matchExp: MatchExp,
|
|
182
|
+
modifier?: Modifier,
|
|
183
|
+
attributeQuery?: AttributeQuery
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
// Find multiple records (safe for reading data)
|
|
187
|
+
const records = await system.storage.find(
|
|
188
|
+
entityName: string,
|
|
189
|
+
matchExp: MatchExp,
|
|
190
|
+
modifier?: Modifier,
|
|
191
|
+
attributeQuery?: AttributeQuery
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Update records (ONLY for test setup - bypasses ALL validation!)
|
|
195
|
+
await system.storage.update(
|
|
196
|
+
entityName: string,
|
|
197
|
+
matchExp: MatchExp,
|
|
198
|
+
data: object
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// Delete records (ONLY for test cleanup - bypasses ALL business logic!)
|
|
202
|
+
await system.storage.delete(
|
|
203
|
+
entityName: string,
|
|
204
|
+
matchExp: MatchExp
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### 3. MatchExp Usage
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Simple equality
|
|
212
|
+
MatchExp.atom({ key: 'field', value: ['=', value] })
|
|
213
|
+
|
|
214
|
+
// Multiple conditions (AND)
|
|
215
|
+
MatchExp.atom({ key: 'status', value: ['=', 'active'] })
|
|
216
|
+
.and({ key: 'role', value: ['=', 'admin'] })
|
|
217
|
+
|
|
218
|
+
// OR conditions
|
|
219
|
+
MatchExp.atom({ key: 'role', value: ['=', 'admin'] })
|
|
220
|
+
.or({ key: 'role', value: ['=', 'moderator'] })
|
|
221
|
+
|
|
222
|
+
// Complex operators
|
|
223
|
+
MatchExp.atom({ key: 'age', value: ['>', 18] })
|
|
224
|
+
MatchExp.atom({ key: 'name', value: ['like', '%john%'] })
|
|
225
|
+
MatchExp.atom({ key: 'status', value: ['in', ['active', 'pending']] })
|
|
226
|
+
MatchExp.atom({ key: 'score', value: ['between', [60, 100]] })
|
|
227
|
+
|
|
228
|
+
// Nested field access
|
|
229
|
+
MatchExp.atom({ key: 'user.profile.city', value: ['=', 'Beijing'] })
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Error Handling in Tests
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
test('should handle errors correctly', async () => {
|
|
236
|
+
// ✅ CORRECT: Check error field in result
|
|
237
|
+
const result = await controller.callInteraction('SomeInteraction', {
|
|
238
|
+
user: { id: 'user1' },
|
|
239
|
+
payload: { invalid: 'data' }
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
expect(result.error).toBeDefined()
|
|
243
|
+
expect(result.error.message).toContain('expected error message')
|
|
244
|
+
|
|
245
|
+
// ❌ WRONG: interaqt doesn't throw exceptions
|
|
246
|
+
// try {
|
|
247
|
+
// await controller.callInteraction(...)
|
|
248
|
+
// } catch (e) {
|
|
249
|
+
// // This won't work
|
|
250
|
+
// }
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
# 12. How to Perform Testing
|
|
255
|
+
|
|
256
|
+
Testing is a crucial component for ensuring the quality of interaqt applications. The framework provides comprehensive testing support, including unit testing, integration testing, and end-to-end testing. This chapter will detail how to write effective tests for reactive applications.
|
|
257
|
+
|
|
258
|
+
## ⚠️ CRITICAL: interaqt Testing Philosophy
|
|
259
|
+
|
|
260
|
+
**In the interaqt framework, ALL data is derived from interaction events.** This fundamental principle changes how we approach testing:
|
|
261
|
+
|
|
262
|
+
1. **Focus on Interaction Testing**: Since all Entity and Relation data are created, modified, and deleted through Interactions, comprehensive Interaction testing naturally covers all data operations.
|
|
263
|
+
|
|
264
|
+
2. **No Separate Entity/Relation Tests**: You should NOT write separate unit tests for Entity CRUD operations or Relation creation/deletion. These are implementation details that are automatically tested when you test the Interactions that use them.
|
|
265
|
+
|
|
266
|
+
3. **Coverage Through Interactions**: If your test coverage is below 100% after testing all Interactions, it indicates:
|
|
267
|
+
- Missing Interaction definitions in your design
|
|
268
|
+
- Insufficient edge case testing for existing Interactions
|
|
269
|
+
- Unused code that should be removed
|
|
270
|
+
|
|
271
|
+
4. **Test What Matters**: Test the business logic and user scenarios through Interactions, not the framework mechanics.
|
|
272
|
+
|
|
273
|
+
5. **Storage APIs are LOW-LEVEL**:
|
|
274
|
+
- `storage.create()`, `storage.update()`, `storage.delete()` bypass ALL validation and business logic
|
|
275
|
+
- Use them ONLY for test data setup (creating prerequisite records)
|
|
276
|
+
- NEVER use them to test validation failures - they will always succeed!
|
|
277
|
+
- ALL business logic testing must go through `callInteraction()`
|
|
278
|
+
|
|
279
|
+
## 12.1 Testing Reactive Computations
|
|
280
|
+
|
|
281
|
+
### 12.1.1 Count Computation Testing
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// tests/computations/count.spec.ts
|
|
285
|
+
describe('Count Computation', () => {
|
|
286
|
+
test('should update user count automatically', async () => {
|
|
287
|
+
const system = new MonoSystem(new PGLiteDB());
|
|
288
|
+
|
|
289
|
+
const userEntity = Entity.create({
|
|
290
|
+
name: 'User',
|
|
291
|
+
properties: [
|
|
292
|
+
Property.create({ name: 'username', type: 'string' }),
|
|
293
|
+
Property.create({ name: 'isActive', type: 'boolean' })
|
|
294
|
+
]
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const totalUsersDict = Dictionary.create({
|
|
298
|
+
name: 'totalUsers',
|
|
299
|
+
type: 'number',
|
|
300
|
+
collection: false,
|
|
301
|
+
defaultValue: () => 0,
|
|
302
|
+
computation: Count.create({
|
|
303
|
+
record: userEntity
|
|
304
|
+
})
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const activeUsersDict = Dictionary.create({
|
|
308
|
+
name: 'activeUsers',
|
|
309
|
+
type: 'number',
|
|
310
|
+
collection: false,
|
|
311
|
+
defaultValue: () => 0,
|
|
312
|
+
computation: Count.create({
|
|
313
|
+
record: userEntity
|
|
314
|
+
})
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const controller = new Controller({
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
system: system,
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
entities: [userEntity],
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
relations: [],
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
activities: [],
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
interactions: [],
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
dict: [totalUsersDict, activeUsersDict],
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
recordMutationSideEffects: []
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
});
|
|
342
|
+
await controller.setup(true);
|
|
343
|
+
|
|
344
|
+
// Check initial state
|
|
345
|
+
let totalUsers = await system.storage.get('state', 'totalUsers');
|
|
346
|
+
let activeUsers = await system.storage.get('state', 'activeUsers');
|
|
347
|
+
expect(totalUsers).toBe(0);
|
|
348
|
+
expect(activeUsers).toBe(0);
|
|
349
|
+
|
|
350
|
+
// Create active user
|
|
351
|
+
await system.storage.create('User', {
|
|
352
|
+
username: 'alice',
|
|
353
|
+
isActive: true
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Verify count update
|
|
357
|
+
totalUsers = await system.storage.get('state', 'totalUsers');
|
|
358
|
+
activeUsers = await system.storage.get('state', 'activeUsers');
|
|
359
|
+
expect(totalUsers).toBe(1);
|
|
360
|
+
expect(activeUsers).toBe(1);
|
|
361
|
+
|
|
362
|
+
// Create inactive user
|
|
363
|
+
await system.storage.create('User', {
|
|
364
|
+
username: 'bob',
|
|
365
|
+
isActive: false
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Verify count update
|
|
369
|
+
totalUsers = await system.storage.get('state', 'totalUsers');
|
|
370
|
+
activeUsers = await system.storage.get('state', 'activeUsers');
|
|
371
|
+
expect(totalUsers).toBe(2);
|
|
372
|
+
expect(activeUsers).toBe(1);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### 12.1.2 Complex Computation Testing
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// tests/computations/transform.spec.ts
|
|
381
|
+
describe('Transform Computation', () => {
|
|
382
|
+
test('should calculate user statistics correctly', async () => {
|
|
383
|
+
const system = new MonoSystem(new PGLiteDB());
|
|
384
|
+
|
|
385
|
+
const userEntity = Entity.create({
|
|
386
|
+
name: 'User',
|
|
387
|
+
properties: [
|
|
388
|
+
Property.create({ name: 'username', type: 'string' }),
|
|
389
|
+
Property.create({ name: 'age', type: 'number' }),
|
|
390
|
+
Property.create({ name: 'score', type: 'number' })
|
|
391
|
+
]
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const userStatsDict = Dictionary.create({
|
|
395
|
+
name: 'userStats',
|
|
396
|
+
type: 'object',
|
|
397
|
+
collection: false,
|
|
398
|
+
defaultValue: () => ({}),
|
|
399
|
+
computation: Transform.create({
|
|
400
|
+
record: userEntity,
|
|
401
|
+
attributeQuery: ['age', 'score'],
|
|
402
|
+
callback: (users: any[]) => {
|
|
403
|
+
if (users.length === 0) {
|
|
404
|
+
return {
|
|
405
|
+
totalUsers: 0,
|
|
406
|
+
averageAge: 0,
|
|
407
|
+
averageScore: 0,
|
|
408
|
+
maxScore: 0,
|
|
409
|
+
minScore: 0
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
|
|
414
|
+
const totalScore = users.reduce((sum, user) => sum + user.score, 0);
|
|
415
|
+
const scores = users.map(user => user.score);
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
totalUsers: users.length,
|
|
419
|
+
averageAge: totalAge / users.length,
|
|
420
|
+
averageScore: totalScore / users.length,
|
|
421
|
+
maxScore: Math.max(...scores),
|
|
422
|
+
minScore: Math.min(...scores)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const controller = new Controller({
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
system: system,
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
entities: [userEntity],
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
relations: [],
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
activities: [],
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
interactions: [],
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
dict: [userStatsDict],
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
recordMutationSideEffects: []
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
});
|
|
453
|
+
await controller.setup(true);
|
|
454
|
+
|
|
455
|
+
// Create test users
|
|
456
|
+
await system.storage.create('User', {
|
|
457
|
+
username: 'alice',
|
|
458
|
+
age: 25,
|
|
459
|
+
score: 85
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
await system.storage.create('User', {
|
|
463
|
+
username: 'bob',
|
|
464
|
+
age: 30,
|
|
465
|
+
score: 92
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
await system.storage.create('User', {
|
|
469
|
+
username: 'charlie',
|
|
470
|
+
age: 35,
|
|
471
|
+
score: 78
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Verify statistical calculation
|
|
475
|
+
const stats = await system.storage.get('state', 'userStats');
|
|
476
|
+
expect(stats.totalUsers).toBe(3);
|
|
477
|
+
expect(stats.averageAge).toBe(30);
|
|
478
|
+
expect(stats.averageScore).toBeCloseTo(85);
|
|
479
|
+
expect(stats.maxScore).toBe(92);
|
|
480
|
+
expect(stats.minScore).toBe(78);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## 12.2 Testing Interactions and Activities
|
|
486
|
+
|
|
487
|
+
### 12.2.1 Interaction Testing
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// tests/interactions/userActions.spec.ts
|
|
491
|
+
describe('User Interactions', () => {
|
|
492
|
+
test('should handle user registration interaction', async () => {
|
|
493
|
+
const system = new MonoSystem(new PGLiteDB());
|
|
494
|
+
|
|
495
|
+
const userEntity = Entity.create({
|
|
496
|
+
name: 'User',
|
|
497
|
+
properties: [
|
|
498
|
+
Property.create({ name: 'username', type: 'string' }),
|
|
499
|
+
Property.create({ name: 'email', type: 'string' }),
|
|
500
|
+
Property.create({ name: 'isActive', type: 'boolean', defaultValue: () => true })
|
|
501
|
+
]
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const registerInteraction = Interaction.create({
|
|
505
|
+
name: 'register',
|
|
506
|
+
action: Action.create({ name: 'register' }),
|
|
507
|
+
payload: Payload.create({
|
|
508
|
+
items: [
|
|
509
|
+
PayloadItem.create({
|
|
510
|
+
name: 'userData',
|
|
511
|
+
base: userEntity
|
|
512
|
+
})
|
|
513
|
+
]
|
|
514
|
+
})
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const controller = new Controller({
|
|
518
|
+
system,
|
|
519
|
+
entities: [userEntity],
|
|
520
|
+
relations: [],
|
|
521
|
+
activities: [], // activities
|
|
522
|
+
interactions: [registerInteraction], // interactions
|
|
523
|
+
dict: []
|
|
524
|
+
});
|
|
525
|
+
await controller.setup(true);
|
|
526
|
+
|
|
527
|
+
// Execute registration interaction
|
|
528
|
+
const result = await controller.callInteraction(registerInteraction.name, {
|
|
529
|
+
user: { id: 'test-user' }, // Add user object
|
|
530
|
+
payload: {
|
|
531
|
+
userData: {
|
|
532
|
+
username: 'newuser',
|
|
533
|
+
email: 'newuser@example.com'
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Verify interaction result
|
|
539
|
+
expect(result).toBeTruthy();
|
|
540
|
+
|
|
541
|
+
// Verify user creation
|
|
542
|
+
const user = await system.storage.findOne(
|
|
543
|
+
'User',
|
|
544
|
+
MatchExp.atom({ key: 'username', value: ['=', 'newuser'] }),
|
|
545
|
+
undefined,
|
|
546
|
+
['id', 'username', 'email', 'isActive'] // Specify fields to verify
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
expect(user).toBeTruthy();
|
|
550
|
+
expect(user.username).toBe('newuser');
|
|
551
|
+
expect(user.email).toBe('newuser@example.com');
|
|
552
|
+
expect(user.isActive).toBe(true);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### 12.2.2 Activity Testing
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// tests/activities/approvalProcess.spec.ts
|
|
561
|
+
describe('Approval Process Activity', () => {
|
|
562
|
+
test('should handle complete approval workflow', async () => {
|
|
563
|
+
const system = new MonoSystem(new PGLiteDB());
|
|
564
|
+
|
|
565
|
+
// Create request entity
|
|
566
|
+
const requestEntity = Entity.create({
|
|
567
|
+
name: 'Request',
|
|
568
|
+
properties: [
|
|
569
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
570
|
+
Property.create({ name: 'status', type: 'string' }),
|
|
571
|
+
Property.create({ name: 'submitterId', type: 'string' })
|
|
572
|
+
]
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Create activity states
|
|
576
|
+
const submittedState = StateNode.create({ name: 'submitted' });
|
|
577
|
+
const reviewingState = StateNode.create({ name: 'reviewing' });
|
|
578
|
+
const approvedState = StateNode.create({ name: 'approved' });
|
|
579
|
+
const rejectedState = StateNode.create({ name: 'rejected' });
|
|
580
|
+
|
|
581
|
+
// Create interactions
|
|
582
|
+
const submitInteraction = Interaction.create({
|
|
583
|
+
name: 'submit',
|
|
584
|
+
action: Action.create({ name: 'submit' })
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const approveInteraction = Interaction.create({
|
|
588
|
+
name: 'approve',
|
|
589
|
+
action: Action.create({ name: 'approve' })
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const rejectInteraction = Interaction.create({
|
|
593
|
+
name: 'reject',
|
|
594
|
+
action: Action.create({ name: 'reject' })
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Create state transfers
|
|
598
|
+
const submitTransfer = StateTransfer.create({
|
|
599
|
+
trigger: submitInteraction,
|
|
600
|
+
current: submittedState,
|
|
601
|
+
next: reviewingState
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const approveTransfer = StateTransfer.create({
|
|
605
|
+
trigger: approveInteraction,
|
|
606
|
+
current: reviewingState,
|
|
607
|
+
next: approvedState
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const rejectTransfer = StateTransfer.create({
|
|
611
|
+
trigger: rejectInteraction,
|
|
612
|
+
current: reviewingState,
|
|
613
|
+
next: rejectedState
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Create activity
|
|
617
|
+
const approvalActivity = Activity.create({
|
|
618
|
+
name: 'ApprovalProcess',
|
|
619
|
+
states: [submittedState, reviewingState, approvedState, rejectedState],
|
|
620
|
+
transfers: [submitTransfer, approveTransfer, rejectTransfer],
|
|
621
|
+
defaultState: submittedState
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const controller = new Controller({
|
|
625
|
+
system,
|
|
626
|
+
entities: [requestEntity],
|
|
627
|
+
relations: [],
|
|
628
|
+
activities: [approvalActivity], // activities
|
|
629
|
+
interactions: [submitInteraction, approveInteraction, rejectInteraction], // interactions
|
|
630
|
+
dict: []
|
|
631
|
+
});
|
|
632
|
+
await controller.setup(true);
|
|
633
|
+
|
|
634
|
+
// Create request
|
|
635
|
+
const request = await system.storage.create('Request', {
|
|
636
|
+
title: 'Test Request',
|
|
637
|
+
status: 'submitted',
|
|
638
|
+
submitterId: 'user123'
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Execute submit interaction
|
|
642
|
+
await controller.callInteraction(submitInteraction.name, {
|
|
643
|
+
user: { id: 'user123' }, // Add user object
|
|
644
|
+
payload: {
|
|
645
|
+
requestId: request.id
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Verify state transition
|
|
650
|
+
let updatedRequest = await system.storage.findOne(
|
|
651
|
+
'Request',
|
|
652
|
+
MatchExp.atom({ key: 'id', value: ['=', request.id] }),
|
|
653
|
+
undefined,
|
|
654
|
+
['id', 'title', 'status', 'submitterId'] // Specify fields
|
|
655
|
+
);
|
|
656
|
+
expect(updatedRequest.status).toBe('reviewing');
|
|
657
|
+
|
|
658
|
+
// Execute approve interaction
|
|
659
|
+
await controller.callInteraction(approveInteraction.name, {
|
|
660
|
+
user: { id: 'admin-user' }, // Add user object
|
|
661
|
+
payload: {
|
|
662
|
+
requestId: request.id
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Verify final state
|
|
667
|
+
updatedRequest = await system.storage.findOne(
|
|
668
|
+
'Request',
|
|
669
|
+
MatchExp.atom({ key: 'id', value: ['=', request.id] }),
|
|
670
|
+
undefined,
|
|
671
|
+
['id', 'title', 'status', 'submitterId'] // Specify fields
|
|
672
|
+
);
|
|
673
|
+
expect(updatedRequest.status).toBe('approved');
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
## 12.3 Testing Permissions and Attributives
|
|
679
|
+
|
|
680
|
+
> **Important: Correct Error Handling Approach**
|
|
681
|
+
>
|
|
682
|
+
> The interaqt framework automatically catches all errors (including Attributive validation failures, insufficient permissions, etc.) and returns error information through the `error` field in the return value. The framework **does not throw uncaught exceptions**.
|
|
683
|
+
>
|
|
684
|
+
> Therefore, when writing tests:
|
|
685
|
+
> - ✅ **Correct approach**: Check the `error` field in the return value
|
|
686
|
+
> - ❌ **Wrong approach**: Use try-catch to catch exceptions
|
|
687
|
+
>
|
|
688
|
+
> ```javascript
|
|
689
|
+
> // ✅ Correct testing approach
|
|
690
|
+
> const result = await controller.callInteraction('SomeInteraction', {...});
|
|
691
|
+
> expect(result.error).toBeTruthy();
|
|
692
|
+
> expect(result.error.message).toContain('permission denied');
|
|
693
|
+
>
|
|
694
|
+
> // ❌ Wrong testing approach
|
|
695
|
+
> try {
|
|
696
|
+
> await controller.callInteraction('SomeInteraction', {...});
|
|
697
|
+
> fail('Should have thrown error');
|
|
698
|
+
> } catch (e) {
|
|
699
|
+
> // This code will never execute as the framework doesn't throw exceptions
|
|
700
|
+
> }
|
|
701
|
+
> ```
|
|
702
|
+
|
|
703
|
+
### 12.3.1 Permission Testing Basics
|
|
704
|
+
|
|
705
|
+
Permission testing is an important component of interaqt application testing, requiring verification of access permissions for different users in different scenarios:
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
709
|
+
import { Controller, MonoSystem, KlassByName, PGLiteDB } from 'interaqt';
|
|
710
|
+
|
|
711
|
+
describe('Permission Testing', () => {
|
|
712
|
+
let system: MonoSystem;
|
|
713
|
+
let controller: Controller;
|
|
714
|
+
|
|
715
|
+
beforeEach(async () => {
|
|
716
|
+
system = new MonoSystem(new PGLiteDB());
|
|
717
|
+
system.conceptClass = KlassByName;
|
|
718
|
+
|
|
719
|
+
controller = new Controller({
|
|
720
|
+
system,
|
|
721
|
+
entities,
|
|
722
|
+
relations,
|
|
723
|
+
activities,
|
|
724
|
+
interactions,
|
|
725
|
+
dict: []
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
await controller.setup(true);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
});
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### 12.3.2 Basic Role Permission Testing
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
describe('Basic Role Permission Testing', () => {
|
|
739
|
+
test('admin permission test', async () => {
|
|
740
|
+
// Create admin user
|
|
741
|
+
const admin = await system.storage.create('User', {
|
|
742
|
+
name: 'Admin User',
|
|
743
|
+
role: 'admin',
|
|
744
|
+
email: 'admin@example.com'
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Test that admin can perform privileged operations
|
|
748
|
+
const result = await controller.callInteraction('CreateDormitory', {
|
|
749
|
+
user: admin,
|
|
750
|
+
payload: {
|
|
751
|
+
name: 'Admin Created Dormitory',
|
|
752
|
+
building: 'Admin Building',
|
|
753
|
+
roomNumber: '001',
|
|
754
|
+
capacity: 4,
|
|
755
|
+
description: 'Test dormitory'
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
expect(result.error).toBeUndefined();
|
|
760
|
+
|
|
761
|
+
// Verify dormitory was actually created
|
|
762
|
+
const { MatchExp } = controller.globals;
|
|
763
|
+
const dormitory = await system.storage.findOne('Dormitory',
|
|
764
|
+
MatchExp.atom({ key: 'name', value: ['=', 'Admin Created Dormitory'] }),
|
|
765
|
+
undefined,
|
|
766
|
+
['id', 'name', 'building', 'roomNumber', 'capacity', 'description'] // Specify fields
|
|
767
|
+
);
|
|
768
|
+
expect(dormitory).toBeTruthy();
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
test('regular user permission restriction test', async () => {
|
|
772
|
+
const student = await system.storage.create('User', {
|
|
773
|
+
name: 'Regular Student',
|
|
774
|
+
role: 'student',
|
|
775
|
+
email: 'student@example.com'
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Regular student should not be able to create dormitory
|
|
779
|
+
const result = await controller.callInteraction('CreateDormitory', {
|
|
780
|
+
user: student,
|
|
781
|
+
payload: {
|
|
782
|
+
name: 'Student Attempted Dormitory',
|
|
783
|
+
building: 'Student Building',
|
|
784
|
+
roomNumber: '002',
|
|
785
|
+
capacity: 4,
|
|
786
|
+
description: 'Unauthorized test'
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
expect(result.error).toBeTruthy();
|
|
791
|
+
expect(result.error.message).toContain('Admin'); // Permission error should mention requirement
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### 12.3.3 Complex Permission Logic Testing
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
describe('Complex Permission Logic Testing', () => {
|
|
800
|
+
test('dormitory leader permission test', async () => {
|
|
801
|
+
// Setup test scenario
|
|
802
|
+
const leader = await system.storage.create('User', {
|
|
803
|
+
name: 'Dormitory Leader',
|
|
804
|
+
role: 'student',
|
|
805
|
+
email: 'leader@example.com'
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const member = await system.storage.create('User', {
|
|
809
|
+
name: 'Regular Member',
|
|
810
|
+
role: 'student',
|
|
811
|
+
email: 'member@example.com'
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// Create dormitory and member relations
|
|
815
|
+
const dormitory = await system.storage.create('Dormitory', {
|
|
816
|
+
name: 'Permission Test Dormitory',
|
|
817
|
+
building: 'Permission Test Building',
|
|
818
|
+
roomNumber: '999',
|
|
819
|
+
capacity: 4
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
const leaderMember = await system.storage.create('DormitoryMember', {
|
|
823
|
+
user: leader,
|
|
824
|
+
dormitory: dormitory,
|
|
825
|
+
role: 'leader',
|
|
826
|
+
status: 'active',
|
|
827
|
+
bedNumber: 1,
|
|
828
|
+
joinedAt: new Date().toISOString()
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const normalMember = await system.storage.create('DormitoryMember', {
|
|
832
|
+
user: member,
|
|
833
|
+
dormitory: dormitory,
|
|
834
|
+
role: 'member',
|
|
835
|
+
status: 'active',
|
|
836
|
+
bedNumber: 2,
|
|
837
|
+
joinedAt: new Date().toISOString()
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// Test that leader can record scores
|
|
841
|
+
const leaderResult = await controller.callInteraction('RecordScore', {
|
|
842
|
+
user: leader,
|
|
843
|
+
payload: {
|
|
844
|
+
memberId: normalMember,
|
|
845
|
+
points: 10,
|
|
846
|
+
reason: 'Cleaning duties',
|
|
847
|
+
category: 'hygiene'
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
expect(leaderResult.error).toBeUndefined();
|
|
851
|
+
|
|
852
|
+
// Test that regular member cannot record scores
|
|
853
|
+
const memberResult = await controller.callInteraction('RecordScore', {
|
|
854
|
+
user: member,
|
|
855
|
+
payload: {
|
|
856
|
+
memberId: leaderMember,
|
|
857
|
+
points: 10,
|
|
858
|
+
reason: 'Attempted score recording',
|
|
859
|
+
category: 'hygiene'
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
expect(memberResult.error).toBeTruthy();
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### 12.3.4 Payload-level Permission Testing
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
describe('Payload-level Permission Testing', () => {
|
|
871
|
+
test('can only operate on own dormitory data', async () => {
|
|
872
|
+
// Create two dormitory leaders
|
|
873
|
+
const leader1 = await system.storage.create('User', {
|
|
874
|
+
name: 'Leader 1',
|
|
875
|
+
role: 'student',
|
|
876
|
+
email: 'leader1@example.com'
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
const leader2 = await system.storage.create('User', {
|
|
880
|
+
name: 'Leader 2',
|
|
881
|
+
role: 'student',
|
|
882
|
+
email: 'leader2@example.com'
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Create two dormitories
|
|
886
|
+
const dormitory1 = await system.storage.create('Dormitory', {
|
|
887
|
+
name: 'Dormitory 1',
|
|
888
|
+
building: 'Test Building',
|
|
889
|
+
roomNumber: '201',
|
|
890
|
+
capacity: 4
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
const dormitory2 = await system.storage.create('Dormitory', {
|
|
894
|
+
name: 'Dormitory 2',
|
|
895
|
+
building: 'Test Building',
|
|
896
|
+
roomNumber: '202',
|
|
897
|
+
capacity: 4
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Establish member relations
|
|
901
|
+
const member1 = await system.storage.create('DormitoryMember', {
|
|
902
|
+
user: leader1,
|
|
903
|
+
dormitory: dormitory1,
|
|
904
|
+
role: 'leader',
|
|
905
|
+
status: 'active',
|
|
906
|
+
bedNumber: 1,
|
|
907
|
+
joinedAt: new Date().toISOString()
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
const member2 = await system.storage.create('DormitoryMember', {
|
|
911
|
+
user: leader2,
|
|
912
|
+
dormitory: dormitory2,
|
|
913
|
+
role: 'leader',
|
|
914
|
+
status: 'active',
|
|
915
|
+
bedNumber: 1,
|
|
916
|
+
joinedAt: new Date().toISOString()
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// Leader 1 should be able to operate on own dormitory members
|
|
920
|
+
const validResult = await controller.callInteraction('RecordScore', {
|
|
921
|
+
user: leader1,
|
|
922
|
+
payload: {
|
|
923
|
+
memberId: member1,
|
|
924
|
+
points: 10,
|
|
925
|
+
reason: 'Cleanliness',
|
|
926
|
+
category: 'hygiene'
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
expect(validResult.error).toBeUndefined();
|
|
930
|
+
|
|
931
|
+
// Leader 1 should not be able to operate on other dormitory members
|
|
932
|
+
const invalidResult = await controller.callInteraction('RecordScore', {
|
|
933
|
+
user: leader1,
|
|
934
|
+
payload: {
|
|
935
|
+
memberId: member2,
|
|
936
|
+
points: 10,
|
|
937
|
+
reason: 'Cross-dormitory operation attempt',
|
|
938
|
+
category: 'hygiene'
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
expect(invalidResult.error).toBeTruthy();
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### 12.3.5 Permission Edge Case Testing
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
describe('Permission Edge Case Testing', () => {
|
|
950
|
+
test('application restriction when dormitory is full', async () => {
|
|
951
|
+
const student = await system.storage.create('User', {
|
|
952
|
+
name: 'Applicant Student',
|
|
953
|
+
role: 'student',
|
|
954
|
+
email: 'applicant@example.com'
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// Create full dormitory
|
|
958
|
+
const fullDormitory = await system.storage.create('Dormitory', {
|
|
959
|
+
name: 'Full Dormitory',
|
|
960
|
+
building: 'Test Building',
|
|
961
|
+
roomNumber: '301',
|
|
962
|
+
capacity: 2
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// Add members until full
|
|
966
|
+
for (let i = 0; i < 2; i++) {
|
|
967
|
+
const user = await system.storage.create('User', {
|
|
968
|
+
name: `Member ${i + 1}`,
|
|
969
|
+
role: 'student',
|
|
970
|
+
email: `member${i + 1}@example.com`
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
await system.storage.create('DormitoryMember', {
|
|
974
|
+
user: user,
|
|
975
|
+
dormitory: fullDormitory,
|
|
976
|
+
role: 'member',
|
|
977
|
+
status: 'active',
|
|
978
|
+
bedNumber: i + 1,
|
|
979
|
+
joinedAt: new Date().toISOString()
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Try to apply to full dormitory
|
|
984
|
+
const result = await controller.callInteraction('ApplyForDormitory', {
|
|
985
|
+
user: student,
|
|
986
|
+
payload: {
|
|
987
|
+
dormitoryId: fullDormitory,
|
|
988
|
+
message: 'Hope to join this dormitory'
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
expect(result.error).toBeTruthy();
|
|
993
|
+
expect(result.error.message).toContain('DormitoryNotFull');
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
test('duplicate application restriction', async () => {
|
|
997
|
+
const student = await system.storage.create('User', {
|
|
998
|
+
name: 'Student with Dormitory',
|
|
999
|
+
role: 'student',
|
|
1000
|
+
email: 'hasdorm@example.com'
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// Create dormitories
|
|
1004
|
+
const dormitory1 = await system.storage.create('Dormitory', {
|
|
1005
|
+
name: 'Current Dormitory',
|
|
1006
|
+
building: 'Test Building',
|
|
1007
|
+
roomNumber: '401',
|
|
1008
|
+
capacity: 4
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const dormitory2 = await system.storage.create('Dormitory', {
|
|
1012
|
+
name: 'Target Dormitory',
|
|
1013
|
+
building: 'Test Building',
|
|
1014
|
+
roomNumber: '402',
|
|
1015
|
+
capacity: 4
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// Student already in dormitory1
|
|
1019
|
+
await system.storage.create('DormitoryMember', {
|
|
1020
|
+
user: student,
|
|
1021
|
+
dormitory: dormitory1,
|
|
1022
|
+
role: 'member',
|
|
1023
|
+
status: 'active',
|
|
1024
|
+
bedNumber: 1,
|
|
1025
|
+
joinedAt: new Date().toISOString()
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// Try to apply to dormitory2
|
|
1029
|
+
const result = await controller.callInteraction('ApplyForDormitory', {
|
|
1030
|
+
user: student,
|
|
1031
|
+
payload: {
|
|
1032
|
+
dormitoryId: dormitory2,
|
|
1033
|
+
message: 'Want to change dormitory'
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
expect(result.error).toBeTruthy();
|
|
1038
|
+
expect(result.error.message).toContain('NoActiveDormitory');
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### 12.3.6 State Machine Permission Testing
|
|
1044
|
+
|
|
1045
|
+
```typescript
|
|
1046
|
+
describe('State Machine Permission Testing', () => {
|
|
1047
|
+
test('state machine computeTarget function coverage test', async () => {
|
|
1048
|
+
// Create admin and target user
|
|
1049
|
+
const admin = await system.storage.create('User', {
|
|
1050
|
+
name: 'State Machine Test Admin',
|
|
1051
|
+
role: 'admin',
|
|
1052
|
+
email: 'statemachine@test.com'
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
const targetUser = await system.storage.create('User', {
|
|
1056
|
+
name: 'Student to be Kicked',
|
|
1057
|
+
role: 'student',
|
|
1058
|
+
email: 'target@test.com',
|
|
1059
|
+
studentId: 'TARGET001'
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Create dormitory and member
|
|
1063
|
+
const dormitory = await system.storage.create('Dormitory', {
|
|
1064
|
+
name: 'State Machine Test Dormitory',
|
|
1065
|
+
building: 'State Machine Test Building',
|
|
1066
|
+
roomNumber: '999',
|
|
1067
|
+
capacity: 4
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const targetMember = await system.storage.create('DormitoryMember', {
|
|
1071
|
+
user: targetUser,
|
|
1072
|
+
dormitory: dormitory,
|
|
1073
|
+
role: 'member',
|
|
1074
|
+
status: 'active',
|
|
1075
|
+
score: -60,
|
|
1076
|
+
bedNumber: 1,
|
|
1077
|
+
joinedAt: new Date().toISOString()
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// Create kick request
|
|
1081
|
+
const kickRequest = await system.storage.create('KickRequest', {
|
|
1082
|
+
targetMember: targetMember,
|
|
1083
|
+
requester: admin,
|
|
1084
|
+
reason: 'Violated dormitory rules, score too low',
|
|
1085
|
+
status: 'pending',
|
|
1086
|
+
createdAt: new Date().toISOString()
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// Execute ApproveKickRequest interaction, trigger state machine
|
|
1090
|
+
const result = await controller.callInteraction('ApproveKickRequest', {
|
|
1091
|
+
user: admin,
|
|
1092
|
+
payload: {
|
|
1093
|
+
kickRequestId: kickRequest,
|
|
1094
|
+
adminComment: 'Admin approved kick request'
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
expect(result.error).toBeUndefined();
|
|
1099
|
+
|
|
1100
|
+
// Verify state machine successfully executed state transition
|
|
1101
|
+
const { MatchExp } = controller.globals;
|
|
1102
|
+
const updatedMember = await system.storage.findOne('DormitoryMember',
|
|
1103
|
+
MatchExp.atom({ key: 'id', value: ['=', targetMember.id] }),
|
|
1104
|
+
undefined,
|
|
1105
|
+
['status']
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
expect(updatedMember.status).toBe('kicked');
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
### 12.3.7 Permission Debugging and Error Handling Testing
|
|
1114
|
+
|
|
1115
|
+
```typescript
|
|
1116
|
+
describe('Permission Debugging and Error Handling', () => {
|
|
1117
|
+
test('should provide clear permission error messages', async () => {
|
|
1118
|
+
const student = await system.storage.create('User', {
|
|
1119
|
+
name: 'Regular Student',
|
|
1120
|
+
role: 'student',
|
|
1121
|
+
email: 'student@example.com'
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
const result = await controller.callInteraction('CreateDormitory', {
|
|
1125
|
+
user: student,
|
|
1126
|
+
payload: {
|
|
1127
|
+
name: 'Test Dormitory',
|
|
1128
|
+
building: 'Test Building',
|
|
1129
|
+
roomNumber: '101',
|
|
1130
|
+
capacity: 4,
|
|
1131
|
+
description: 'Test dormitory'
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
expect(result.error).toBeTruthy();
|
|
1136
|
+
expect(result.error.message).toContain('Admin');
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
test('permission checks should handle database query errors', async () => {
|
|
1140
|
+
const student = await system.storage.create('User', {
|
|
1141
|
+
name: 'Test Student',
|
|
1142
|
+
role: 'student',
|
|
1143
|
+
email: 'test@example.com'
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
// Pass invalid ID to trigger query error
|
|
1147
|
+
const result = await controller.callInteraction('RecordScore', {
|
|
1148
|
+
user: student,
|
|
1149
|
+
payload: {
|
|
1150
|
+
memberId: { id: 'invalid-member-id' },
|
|
1151
|
+
points: 10,
|
|
1152
|
+
reason: 'Test error handling',
|
|
1153
|
+
category: 'hygiene'
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
expect(result.error).toBeTruthy();
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
## 12.4 Testing Best Practices
|
|
1163
|
+
|
|
1164
|
+
### 12.4.1 Test Organization (Interaction-Focused)
|
|
1165
|
+
|
|
1166
|
+
```typescript
|
|
1167
|
+
// Organize tests by Interactions, not by data structures
|
|
1168
|
+
describe('User Management Interactions', () => {
|
|
1169
|
+
describe('CreateUser Interaction', () => {
|
|
1170
|
+
test('should create user with valid data', () => {});
|
|
1171
|
+
test('should reject invalid email format', () => {});
|
|
1172
|
+
test('should enforce unique username constraint', () => {});
|
|
1173
|
+
test('should update userCount computation', () => {});
|
|
1174
|
+
test('should require admin permission', () => {});
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
describe('CreateFriendship Interaction', () => {
|
|
1178
|
+
test('should establish friendship between users', () => {});
|
|
1179
|
+
test('should update both users friend count', () => {});
|
|
1180
|
+
test('should handle symmetric relationship correctly', () => {});
|
|
1181
|
+
test('should prevent duplicate friendships', () => {});
|
|
1182
|
+
test('should require both users consent', () => {});
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
describe('UpdateUserScore Interaction', () => {
|
|
1186
|
+
test('should update user score correctly', () => {});
|
|
1187
|
+
test('should trigger score-based computations', () => {});
|
|
1188
|
+
test('should validate score range', () => {});
|
|
1189
|
+
test('should require moderator permission', () => {});
|
|
1190
|
+
test('should create audit log entry', () => {});
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
describe('Edge Cases and Error Scenarios', () => {
|
|
1194
|
+
test('should handle concurrent friend requests', () => {});
|
|
1195
|
+
test('should handle database connection failures gracefully', () => {});
|
|
1196
|
+
test('should provide meaningful error messages', () => {});
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
Testing is a crucial aspect of building reliable interaqt applications. By focusing on comprehensive Interaction testing, developers can ensure their reactive applications work correctly and maintain quality as they evolve. Remember: in interaqt, all data flows from Interactions, so testing Interactions thoroughly is sufficient to achieve complete test coverage. Skip entity and relation unit tests - they're automatically covered when you test the Interactions that use them. Proper test organization, edge case coverage, and permission testing make tests maintainable and effective for long-term development.
|