@objectstack/client 4.0.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,939 +0,0 @@
1
- # @objectstack/client - Server Integration Test Specification
2
-
3
- ## Overview
4
-
5
- This document defines comprehensive integration tests for validating `@objectstack/client` against a live ObjectStack server implementation. These tests verify that the client SDK correctly communicates with the server across all API namespaces.
6
-
7
- ---
8
-
9
- ## Test Environment Setup
10
-
11
- ### Prerequisites
12
-
13
- 1. **Server Requirements:**
14
- - ObjectStack server instance running
15
- - Test database (SQLite/Postgres) with sample data
16
- - All core services enabled (metadata, data, auth)
17
- - Optional services enabled (workflow, ai, realtime, etc.)
18
-
19
- 2. **Client Configuration:**
20
- ```typescript
21
- const testConfig: ClientConfig = {
22
- baseUrl: process.env.TEST_SERVER_URL || 'http://localhost:3000',
23
- token: undefined, // Will be set after login
24
- debug: true,
25
- logger: createLogger({ level: 'debug' })
26
- };
27
- ```
28
-
29
- 3. **Test Data:**
30
- - Sample objects: `test_contact`, `test_project`, `test_task`
31
- - Sample users: test@example.com (admin), user@example.com (standard)
32
- - Sample packages: `@test/sample-plugin`
33
-
34
- ---
35
-
36
- ## Test Suite Structure
37
-
38
- ```
39
- packages/client/tests/integration/
40
- ├── 01-discovery.test.ts # Discovery & connection
41
- ├── 02-auth.test.ts # Authentication flows
42
- ├── 03-metadata.test.ts # Metadata operations
43
- ├── 04-data-crud.test.ts # Basic CRUD operations
44
- ├── 05-data-batch.test.ts # Batch operations
45
- ├── 06-data-query.test.ts # Advanced queries
46
- ├── 07-permissions.test.ts # Permission checking
47
- ├── 08-workflow.test.ts # Workflow operations
48
- ├── 09-realtime.test.ts # Realtime subscriptions
49
- ├── 10-notifications.test.ts # Notifications
50
- ├── 11-ai.test.ts # AI services
51
- ├── 12-i18n.test.ts # Internationalization
52
- ├── 13-analytics.test.ts # Analytics queries
53
- ├── 14-packages.test.ts # Package management
54
- ├── 15-views.test.ts # View management
55
- ├── 16-storage.test.ts # File storage
56
- ├── 17-automation.test.ts # Automation triggers
57
- └── helpers/
58
- ├── test-server.ts # Mock/stub server helpers
59
- ├── test-data.ts # Test data generators
60
- └── assertions.ts # Custom assertions
61
- ```
62
-
63
- ---
64
-
65
- ## Test Cases
66
-
67
- ### 1. Discovery & Connection (`01-discovery.test.ts`)
68
-
69
- #### TC-DISC-001: Standard Discovery via .well-known
70
- ```typescript
71
- describe('Discovery via .well-known', () => {
72
- test('should discover API from .well-known/objectstack', async () => {
73
- const client = new ObjectStackClient({
74
- baseUrl: 'http://localhost:3000'
75
- });
76
-
77
- const discovery = await client.connect();
78
-
79
- expect(discovery.version).toBe('v1');
80
- expect(discovery.apiName).toBe('ObjectStack');
81
- expect(discovery.capabilities).toBeDefined();
82
- expect(discovery.endpoints).toBeDefined();
83
- });
84
- });
85
- ```
86
-
87
- #### TC-DISC-002: Fallback Discovery via /api/v1
88
- ```typescript
89
- test('should fallback to /api/v1 when .well-known unavailable', async () => {
90
- // Mock .well-known to return 404
91
- mockServer.get('/.well-known/objectstack').reply(404);
92
- mockServer.get('/api/v1').reply(200, {
93
- version: 'v1',
94
- apiName: 'ObjectStack'
95
- });
96
-
97
- const client = new ObjectStackClient({ baseUrl: mockServerUrl });
98
- const discovery = await client.connect();
99
-
100
- expect(discovery.version).toBe('v1');
101
- });
102
- ```
103
-
104
- #### TC-DISC-003: Connection Failure Handling
105
- ```typescript
106
- test('should throw error when both discovery methods fail', async () => {
107
- mockServer.get('/.well-known/objectstack').reply(404);
108
- mockServer.get('/api/v1').reply(503);
109
-
110
- const client = new ObjectStackClient({ baseUrl: mockServerUrl });
111
-
112
- await expect(client.connect()).rejects.toThrow(/Failed to connect/);
113
- });
114
- ```
115
-
116
- ---
117
-
118
- ### 2. Authentication (`02-auth.test.ts`)
119
-
120
- #### TC-AUTH-001: Email/Password Login
121
- ```typescript
122
- test('should login with email and password', async () => {
123
- const client = new ObjectStackClient({ baseUrl: testServerUrl });
124
-
125
- const session = await client.auth.login({
126
- method: 'email',
127
- email: 'test@example.com',
128
- password: 'TestPassword123!'
129
- });
130
-
131
- expect(session.token).toBeDefined();
132
- expect(session.user).toBeDefined();
133
- expect(session.user.email).toBe('test@example.com');
134
- expect(session.expiresAt).toBeDefined();
135
- });
136
- ```
137
-
138
- #### TC-AUTH-002: Registration
139
- ```typescript
140
- test('should register new user account', async () => {
141
- const client = new ObjectStackClient({ baseUrl: testServerUrl });
142
-
143
- const session = await client.auth.register({
144
- email: 'newuser@example.com',
145
- password: 'SecurePass123!',
146
- firstName: 'New',
147
- lastName: 'User'
148
- });
149
-
150
- expect(session.token).toBeDefined();
151
- expect(session.user.email).toBe('newuser@example.com');
152
- });
153
- ```
154
-
155
- #### TC-AUTH-003: Token Refresh
156
- ```typescript
157
- test('should refresh expired token', async () => {
158
- const client = new ObjectStackClient({
159
- baseUrl: testServerUrl,
160
- token: expiredToken
161
- });
162
-
163
- const newSession = await client.auth.refreshToken({
164
- refreshToken: validRefreshToken
165
- });
166
-
167
- expect(newSession.token).not.toBe(expiredToken);
168
- expect(newSession.expiresAt).toBeGreaterThan(Date.now());
169
- });
170
- ```
171
-
172
- #### TC-AUTH-004: Get Current User
173
- ```typescript
174
- test('should get current authenticated user', async () => {
175
- const client = new ObjectStackClient({
176
- baseUrl: testServerUrl,
177
- token: validToken
178
- });
179
-
180
- const user = await client.auth.me();
181
-
182
- expect(user.id).toBeDefined();
183
- expect(user.email).toBe('test@example.com');
184
- expect(user.roles).toContain('admin');
185
- });
186
- ```
187
-
188
- #### TC-AUTH-005: Logout
189
- ```typescript
190
- test('should logout and invalidate session', async () => {
191
- const client = new ObjectStackClient({
192
- baseUrl: testServerUrl,
193
- token: validToken
194
- });
195
-
196
- await client.auth.logout();
197
-
198
- // Subsequent requests should fail with 401
199
- await expect(client.auth.me()).rejects.toThrow(/Unauthorized/);
200
- });
201
- ```
202
-
203
- ---
204
-
205
- ### 3. Metadata Operations (`03-metadata.test.ts`)
206
-
207
- #### TC-META-001: Get Metadata Types
208
- ```typescript
209
- test('should retrieve all metadata types', async () => {
210
- const client = await createAuthenticatedClient();
211
-
212
- const types = await client.meta.getTypes();
213
-
214
- expect(types.types).toContain('object');
215
- expect(types.types).toContain('plugin');
216
- expect(types.types).toContain('view');
217
- expect(types.types).toContain('workflow');
218
- });
219
- ```
220
-
221
- #### TC-META-002: Get Items of Type
222
- ```typescript
223
- test('should retrieve all objects', async () => {
224
- const client = await createAuthenticatedClient();
225
-
226
- const objects = await client.meta.getItems('object');
227
-
228
- expect(objects.items).toBeDefined();
229
- expect(objects.items.length).toBeGreaterThan(0);
230
- expect(objects.items[0].name).toBeDefined();
231
- expect(objects.items[0].label).toBeDefined();
232
- });
233
- ```
234
-
235
- #### TC-META-003: Get Specific Object Definition
236
- ```typescript
237
- test('should retrieve object definition by name', async () => {
238
- const client = await createAuthenticatedClient();
239
-
240
- const contactObject = await client.meta.getItem('object', 'test_contact');
241
-
242
- expect(contactObject.name).toBe('test_contact');
243
- expect(contactObject.label).toBe('Contact');
244
- expect(contactObject.fields).toBeDefined();
245
- expect(contactObject.fields.first_name).toBeDefined();
246
- expect(contactObject.fields.first_name.type).toBe('text');
247
- });
248
- ```
249
-
250
- #### TC-META-004: Save Object Definition
251
- ```typescript
252
- test('should create/update object definition', async () => {
253
- const client = await createAuthenticatedClient();
254
-
255
- const newObject = {
256
- name: 'test_dynamic',
257
- label: 'Dynamic Test',
258
- fields: {
259
- name: { type: 'text', label: 'Name', required: true },
260
- status: { type: 'select', label: 'Status', options: ['active', 'inactive'] }
261
- }
262
- };
263
-
264
- const saved = await client.meta.saveItem('object', 'test_dynamic', newObject);
265
-
266
- expect(saved.name).toBe('test_dynamic');
267
- expect(saved.fields.name).toBeDefined();
268
- });
269
- ```
270
-
271
- #### TC-META-005: Metadata Caching with ETag
272
- ```typescript
273
- test('should support ETag-based caching', async () => {
274
- const client = await createAuthenticatedClient();
275
-
276
- // First request
277
- const first = await client.meta.getCached('test_contact');
278
- expect(first.data).toBeDefined();
279
- expect(first.etag).toBeDefined();
280
- expect(first.notModified).toBe(false);
281
-
282
- // Second request with ETag
283
- const second = await client.meta.getCached('test_contact', {
284
- ifNoneMatch: `"${first.etag!.value}"`
285
- });
286
-
287
- expect(second.notModified).toBe(true);
288
- expect(second.data).toBeUndefined();
289
- });
290
- ```
291
-
292
- ---
293
-
294
- ### 4. Data CRUD Operations (`04-data-crud.test.ts`)
295
-
296
- #### TC-DATA-001: Create Record
297
- ```typescript
298
- test('should create new record', async () => {
299
- const client = await createAuthenticatedClient();
300
-
301
- const contact = await client.data.create('test_contact', {
302
- first_name: 'John',
303
- last_name: 'Doe',
304
- email: 'john.doe@example.com',
305
- phone: '+1234567890'
306
- });
307
-
308
- expect(contact.id).toBeDefined();
309
- expect(contact.first_name).toBe('John');
310
- expect(contact.created_at).toBeDefined();
311
- });
312
- ```
313
-
314
- #### TC-DATA-002: Get Record by ID
315
- ```typescript
316
- test('should retrieve record by ID', async () => {
317
- const client = await createAuthenticatedClient();
318
- const created = await client.data.create('test_contact', testContactData);
319
-
320
- const retrieved = await client.data.get('test_contact', created.id);
321
-
322
- expect(retrieved.id).toBe(created.id);
323
- expect(retrieved.first_name).toBe(testContactData.first_name);
324
- });
325
- ```
326
-
327
- #### TC-DATA-003: Update Record
328
- ```typescript
329
- test('should update existing record', async () => {
330
- const client = await createAuthenticatedClient();
331
- const contact = await client.data.create('test_contact', testContactData);
332
-
333
- const updated = await client.data.update('test_contact', contact.id, {
334
- phone: '+9876543210',
335
- notes: 'Updated via test'
336
- });
337
-
338
- expect(updated.id).toBe(contact.id);
339
- expect(updated.phone).toBe('+9876543210');
340
- expect(updated.notes).toBe('Updated via test');
341
- expect(updated.first_name).toBe(testContactData.first_name); // Unchanged
342
- });
343
- ```
344
-
345
- #### TC-DATA-004: Delete Record
346
- ```typescript
347
- test('should delete record', async () => {
348
- const client = await createAuthenticatedClient();
349
- const contact = await client.data.create('test_contact', testContactData);
350
-
351
- await client.data.delete('test_contact', contact.id);
352
-
353
- await expect(
354
- client.data.get('test_contact', contact.id)
355
- ).rejects.toThrow(/Not Found|404/);
356
- });
357
- ```
358
-
359
- #### TC-DATA-005: Find Records with Filters
360
- ```typescript
361
- test('should find records with filters', async () => {
362
- const client = await createAuthenticatedClient();
363
-
364
- // Create test data
365
- await client.data.create('test_contact', { first_name: 'Alice', status: 'active' });
366
- await client.data.create('test_contact', { first_name: 'Bob', status: 'inactive' });
367
- await client.data.create('test_contact', { first_name: 'Charlie', status: 'active' });
368
-
369
- const results = await client.data.find('test_contact', {
370
- filters: { status: 'active' },
371
- sort: 'first_name',
372
- top: 10
373
- });
374
-
375
- expect(results.data.length).toBe(2);
376
- expect(results.data[0].first_name).toBe('Alice');
377
- expect(results.data[1].first_name).toBe('Charlie');
378
- expect(results.total).toBeGreaterThanOrEqual(2);
379
- });
380
- ```
381
-
382
- #### TC-DATA-006: Pagination
383
- ```typescript
384
- test('should support pagination', async () => {
385
- const client = await createAuthenticatedClient();
386
-
387
- // Create 25 test contacts
388
- for (let i = 0; i < 25; i++) {
389
- await client.data.create('test_contact', {
390
- first_name: `Contact${i}`,
391
- email: `contact${i}@example.com`
392
- });
393
- }
394
-
395
- // Page 1
396
- const page1 = await client.data.find('test_contact', {
397
- top: 10,
398
- skip: 0,
399
- sort: 'first_name'
400
- });
401
- expect(page1.data.length).toBe(10);
402
- expect(page1.hasMore).toBe(true);
403
-
404
- // Page 2
405
- const page2 = await client.data.find('test_contact', {
406
- top: 10,
407
- skip: 10,
408
- sort: 'first_name'
409
- });
410
- expect(page2.data.length).toBe(10);
411
- expect(page2.data[0].first_name).not.toBe(page1.data[0].first_name);
412
- });
413
- ```
414
-
415
- ---
416
-
417
- ### 5. Batch Operations (`05-data-batch.test.ts`)
418
-
419
- #### TC-BATCH-001: Create Many Records
420
- ```typescript
421
- test('should create multiple records', async () => {
422
- const client = await createAuthenticatedClient();
423
-
424
- const contacts = [
425
- { first_name: 'Alice', email: 'alice@example.com' },
426
- { first_name: 'Bob', email: 'bob@example.com' },
427
- { first_name: 'Charlie', email: 'charlie@example.com' }
428
- ];
429
-
430
- const created = await client.data.createMany('test_contact', contacts);
431
-
432
- expect(created.length).toBe(3);
433
- expect(created[0].id).toBeDefined();
434
- expect(created[0].first_name).toBe('Alice');
435
- });
436
- ```
437
-
438
- #### TC-BATCH-002: Update Many Records
439
- ```typescript
440
- test('should update multiple records', async () => {
441
- const client = await createAuthenticatedClient();
442
-
443
- // Create test records
444
- const c1 = await client.data.create('test_contact', { first_name: 'Test1' });
445
- const c2 = await client.data.create('test_contact', { first_name: 'Test2' });
446
-
447
- const result = await client.data.updateMany('test_contact', [
448
- { id: c1.id, data: { status: 'updated' } },
449
- { id: c2.id, data: { status: 'updated' } }
450
- ]);
451
-
452
- expect(result.success).toBe(true);
453
- expect(result.successCount).toBe(2);
454
- expect(result.failedCount).toBe(0);
455
- });
456
- ```
457
-
458
- #### TC-BATCH-003: Delete Many Records
459
- ```typescript
460
- test('should delete multiple records', async () => {
461
- const client = await createAuthenticatedClient();
462
-
463
- const c1 = await client.data.create('test_contact', { first_name: 'Delete1' });
464
- const c2 = await client.data.create('test_contact', { first_name: 'Delete2' });
465
-
466
- const result = await client.data.deleteMany('test_contact', [c1.id, c2.id]);
467
-
468
- expect(result.success).toBe(true);
469
- expect(result.successCount).toBe(2);
470
-
471
- await expect(client.data.get('test_contact', c1.id)).rejects.toThrow();
472
- await expect(client.data.get('test_contact', c2.id)).rejects.toThrow();
473
- });
474
- ```
475
-
476
- #### TC-BATCH-004: Mixed Batch Operations
477
- ```typescript
478
- test('should execute mixed batch operations', async () => {
479
- const client = await createAuthenticatedClient();
480
-
481
- const existing = await client.data.create('test_contact', { first_name: 'Existing' });
482
-
483
- const batchRequest: BatchUpdateRequest = {
484
- operations: [
485
- { action: 'create', data: { first_name: 'New1' } },
486
- { action: 'update', id: existing.id, data: { first_name: 'Updated' } },
487
- { action: 'create', data: { first_name: 'New2' } }
488
- ],
489
- options: {
490
- continueOnError: true,
491
- returnData: true
492
- }
493
- };
494
-
495
- const result = await client.data.batch('test_contact', batchRequest);
496
-
497
- expect(result.success).toBe(true);
498
- expect(result.successCount).toBe(3);
499
- expect(result.results).toHaveLength(3);
500
- });
501
- ```
502
-
503
- #### TC-BATCH-005: Transaction Rollback on Error
504
- ```typescript
505
- test('should rollback batch on error when continueOnError=false', async () => {
506
- const client = await createAuthenticatedClient();
507
-
508
- const batchRequest: BatchUpdateRequest = {
509
- operations: [
510
- { action: 'create', data: { first_name: 'Valid1' } },
511
- { action: 'update', id: 'invalid-id', data: { first_name: 'Invalid' } }, // This will fail
512
- { action: 'create', data: { first_name: 'Valid2' } }
513
- ],
514
- options: {
515
- continueOnError: false,
516
- transactional: true
517
- }
518
- };
519
-
520
- await expect(
521
- client.data.batch('test_contact', batchRequest)
522
- ).rejects.toThrow();
523
-
524
- // Verify no records were created (rolled back)
525
- const all = await client.data.find('test_contact', {
526
- filters: { first_name: ['Valid1', 'Valid2'] }
527
- });
528
- expect(all.data.length).toBe(0);
529
- });
530
- ```
531
-
532
- ---
533
-
534
- ### 6. Advanced Queries (`06-data-query.test.ts`)
535
-
536
- #### TC-QUERY-001: ObjectQL AST Query
537
- ```typescript
538
- test('should execute ObjectQL AST query', async () => {
539
- const client = await createAuthenticatedClient();
540
-
541
- const query: Partial<QueryAST> = {
542
- object: 'test_contact',
543
- filter: {
544
- and: [
545
- { field: 'status', operator: 'eq', value: 'active' },
546
- { field: 'created_at', operator: 'gte', value: '2024-01-01' }
547
- ]
548
- },
549
- sort: [
550
- { field: 'last_name', direction: 'asc' },
551
- { field: 'first_name', direction: 'asc' }
552
- ],
553
- pagination: { limit: 20, offset: 0 }
554
- };
555
-
556
- const results = await client.data.query('test_contact', query);
557
-
558
- expect(results.data).toBeDefined();
559
- expect(results.total).toBeGreaterThanOrEqual(0);
560
- });
561
- ```
562
-
563
- #### TC-QUERY-002: Query with Joins/Lookups
564
- ```typescript
565
- test('should query with lookup field expansion', async () => {
566
- const client = await createAuthenticatedClient();
567
-
568
- // Create related data
569
- const project = await client.data.create('test_project', { name: 'Test Project' });
570
- const task = await client.data.create('test_task', {
571
- title: 'Test Task',
572
- project_id: project.id
573
- });
574
-
575
- const query: Partial<QueryAST> = {
576
- object: 'test_task',
577
- expand: ['project_id'], // Expand the lookup field
578
- filter: { field: 'id', operator: 'eq', value: task.id }
579
- };
580
-
581
- const results = await client.data.query('test_task', query);
582
-
583
- expect(results.data[0].project_id).toBeDefined();
584
- expect(results.data[0].project_id.name).toBe('Test Project');
585
- });
586
- ```
587
-
588
- #### TC-QUERY-003: Aggregation Query
589
- ```typescript
590
- test('should execute aggregation query', async () => {
591
- const client = await createAuthenticatedClient();
592
-
593
- const query: Partial<QueryAST> = {
594
- object: 'test_contact',
595
- aggregations: [
596
- { function: 'count', alias: 'total_contacts' },
597
- { function: 'count', field: 'status', alias: 'contacts_with_status' }
598
- ],
599
- groupBy: ['status']
600
- };
601
-
602
- const results = await client.data.query('test_contact', query);
603
-
604
- expect(results.aggregations).toBeDefined();
605
- expect(results.aggregations!.total_contacts).toBeGreaterThan(0);
606
- });
607
- ```
608
-
609
- ---
610
-
611
- ### 7. Permissions (`07-permissions.test.ts`)
612
-
613
- #### TC-PERM-001: Check Create Permission
614
- ```typescript
615
- test('should check if user can create records', async () => {
616
- const client = await createAuthenticatedClient();
617
-
618
- const result = await client.permissions.check({
619
- object: 'test_contact',
620
- action: 'create'
621
- });
622
-
623
- expect(result.allowed).toBe(true);
624
- expect(result.deniedFields).toBeUndefined();
625
- });
626
- ```
627
-
628
- #### TC-PERM-002: Get Object Permissions
629
- ```typescript
630
- test('should retrieve object-level permissions', async () => {
631
- const client = await createAuthenticatedClient();
632
-
633
- const perms = await client.permissions.getObjectPermissions('test_contact');
634
-
635
- expect(perms.object).toBe('test_contact');
636
- expect(perms.permissions).toBeDefined();
637
- expect(perms.fieldPermissions).toBeDefined();
638
- });
639
- ```
640
-
641
- #### TC-PERM-003: Get Effective Permissions
642
- ```typescript
643
- test('should get effective permissions for current user', async () => {
644
- const client = await createAuthenticatedClient();
645
-
646
- const effective = await client.permissions.getEffectivePermissions('test_contact');
647
-
648
- expect(effective.canCreate).toBeDefined();
649
- expect(effective.canRead).toBeDefined();
650
- expect(effective.canEdit).toBeDefined();
651
- expect(effective.canDelete).toBeDefined();
652
- expect(effective.fields).toBeDefined();
653
- });
654
- ```
655
-
656
- ---
657
-
658
- ### 8. Workflow (`08-workflow.test.ts`)
659
-
660
- #### TC-WF-001: Get Workflow Configuration
661
- ```typescript
662
- test('should retrieve workflow rules for object', async () => {
663
- const client = await createAuthenticatedClient();
664
-
665
- const config = await client.workflow.getConfig('test_approval');
666
-
667
- expect(config.object).toBe('test_approval');
668
- expect(config.states).toBeDefined();
669
- expect(config.transitions).toBeDefined();
670
- });
671
- ```
672
-
673
- #### TC-WF-002: Get Workflow State
674
- ```typescript
675
- test('should get current workflow state and available transitions', async () => {
676
- const client = await createAuthenticatedClient();
677
-
678
- const record = await client.data.create('test_approval', {
679
- title: 'Test Approval',
680
- status: 'draft'
681
- });
682
-
683
- const state = await client.workflow.getState('test_approval', record.id);
684
-
685
- expect(state.currentState).toBe('draft');
686
- expect(state.availableTransitions).toContain('submit');
687
- });
688
- ```
689
-
690
- #### TC-WF-003: Execute Workflow Transition
691
- ```typescript
692
- test('should execute workflow state transition', async () => {
693
- const client = await createAuthenticatedClient();
694
-
695
- const record = await client.data.create('test_approval', {
696
- title: 'Test',
697
- status: 'draft'
698
- });
699
-
700
- const result = await client.workflow.transition({
701
- object: 'test_approval',
702
- recordId: record.id,
703
- transition: 'submit',
704
- comment: 'Submitting for approval'
705
- });
706
-
707
- expect(result.success).toBe(true);
708
- expect(result.newState).toBe('pending');
709
- });
710
- ```
711
-
712
- #### TC-WF-004: Approve Workflow
713
- ```typescript
714
- test('should approve workflow transition', async () => {
715
- const client = await createAuthenticatedClient();
716
-
717
- const result = await client.workflow.approve({
718
- object: 'test_approval',
719
- recordId: testRecordId,
720
- comment: 'Approved by manager'
721
- });
722
-
723
- expect(result.success).toBe(true);
724
- expect(result.newState).toBe('approved');
725
- });
726
- ```
727
-
728
- #### TC-WF-005: Reject Workflow
729
- ```typescript
730
- test('should reject workflow transition', async () => {
731
- const client = await createAuthenticatedClient();
732
-
733
- const result = await client.workflow.reject({
734
- object: 'test_approval',
735
- recordId: testRecordId,
736
- reason: 'Insufficient documentation',
737
- comment: 'Please provide more details'
738
- });
739
-
740
- expect(result.success).toBe(true);
741
- expect(result.newState).toBe('rejected');
742
- });
743
- ```
744
-
745
- ---
746
-
747
- ### 9-17. Additional Test Categories
748
-
749
- *(Similar detailed test cases for remaining namespaces: Realtime, Notifications, AI, i18n, Analytics, Packages, Views, Storage, Automation)*
750
-
751
- ---
752
-
753
- ## Test Utilities
754
-
755
- ### Mock Server Setup
756
-
757
- ```typescript
758
- // packages/client/tests/integration/helpers/test-server.ts
759
-
760
- import { setupServer } from 'msw/node';
761
- import { rest } from 'msw';
762
-
763
- export function createMockServer() {
764
- return setupServer(
765
- // Discovery
766
- rest.get('/.well-known/objectstack', (req, res, ctx) => {
767
- return res(ctx.json({
768
- version: 'v1',
769
- apiName: 'ObjectStack Test Server',
770
- capabilities: ['metadata', 'data', 'auth'],
771
- endpoints: { /* ... */ }
772
- }));
773
- }),
774
-
775
- // Auth
776
- rest.post('/api/v1/auth/login', (req, res, ctx) => {
777
- return res(ctx.json({
778
- success: true,
779
- data: {
780
- token: 'mock-jwt-token',
781
- user: { id: '1', email: 'test@example.com' },
782
- expiresAt: Date.now() + 3600000
783
- }
784
- }));
785
- }),
786
-
787
- // Add more handlers...
788
- );
789
- }
790
- ```
791
-
792
- ### Test Data Generators
793
-
794
- ```typescript
795
- // packages/client/tests/integration/helpers/test-data.ts
796
-
797
- export const generateContact = (overrides = {}) => ({
798
- first_name: faker.person.firstName(),
799
- last_name: faker.person.lastName(),
800
- email: faker.internet.email(),
801
- phone: faker.phone.number(),
802
- ...overrides
803
- });
804
-
805
- export const generateProject = (overrides = {}) => ({
806
- name: faker.commerce.productName(),
807
- description: faker.lorem.paragraph(),
808
- status: 'active',
809
- ...overrides
810
- });
811
- ```
812
-
813
- ### Custom Assertions
814
-
815
- ```typescript
816
- // packages/client/tests/integration/helpers/assertions.ts
817
-
818
- export function expectValidId(id: string) {
819
- expect(id).toBeDefined();
820
- expect(typeof id).toBe('string');
821
- expect(id.length).toBeGreaterThan(0);
822
- }
823
-
824
- export function expectValidTimestamp(timestamp: string) {
825
- expect(timestamp).toBeDefined();
826
- expect(new Date(timestamp).getTime()).toBeGreaterThan(0);
827
- }
828
-
829
- export function expectValidResponse<T>(response: any): asserts response is T {
830
- expect(response).toBeDefined();
831
- expect(typeof response).toBe('object');
832
- }
833
- ```
834
-
835
- ---
836
-
837
- ## Running Tests
838
-
839
- ### Local Development
840
-
841
- **Note:** Integration tests require a running ObjectStack server. The server is provided by a separate repository/package and is not included in this spec repository.
842
-
843
- ```bash
844
- # Start test server (in the ObjectStack server repository)
845
- # Follow the server project's documentation for setup
846
- # Example: cd /path/to/objectstack-server && pnpm dev:test
847
-
848
- # Run integration tests (in this repository)
849
- cd packages/client
850
- pnpm test:integration
851
- ```
852
-
853
- ### CI/CD Pipeline
854
-
855
- **Note:** The workflow file referenced below is an example. Actual CI implementation will require setting up the test server infrastructure separately.
856
-
857
- ```yaml
858
- # Example: .github/workflows/client-integration-tests.yml
859
- # This workflow would need to be created and configured with proper server setup
860
- name: Client Integration Tests
861
-
862
- on: [push, pull_request]
863
-
864
- jobs:
865
- test:
866
- runs-on: ubuntu-latest
867
-
868
- services:
869
- postgres:
870
- image: postgres:15
871
- env:
872
- POSTGRES_PASSWORD: test
873
- options: >-
874
- --health-cmd pg_isready
875
- --health-interval 10s
876
-
877
- steps:
878
- - uses: actions/checkout@v3
879
-
880
- - name: Setup Node
881
- uses: actions/setup-node@v3
882
- with:
883
- node-version: 20
884
-
885
- - name: Install dependencies
886
- run: pnpm install
887
-
888
- - name: Build spec
889
- run: pnpm --filter @objectstack/spec build
890
-
891
- # Note: Server setup would require additional configuration
892
- # This is a placeholder showing the expected structure
893
- - name: Start test server
894
- run: |
895
- # Server startup logic would go here
896
- # This depends on the ObjectStack server implementation
897
- echo "Server setup required"
898
- env:
899
- DATABASE_URL: postgresql://postgres:test@localhost:5432/test
900
-
901
- - name: Run integration tests
902
- run: pnpm --filter @objectstack/client test:integration
903
- ```
904
-
905
- ---
906
-
907
- ## Test Coverage Goals
908
-
909
- | Category | Target Coverage | Priority |
910
- |----------|----------------|----------|
911
- | Core Services (discovery, meta, data, auth) | 100% | Critical |
912
- | Optional Services | 90% | High |
913
- | Error Scenarios | 80% | High |
914
- | Edge Cases | 70% | Medium |
915
-
916
- ---
917
-
918
- ## Success Criteria
919
-
920
- - ✅ All 17 test suites pass
921
- - ✅ 90%+ code coverage on client SDK
922
- - ✅ Zero protocol compliance violations
923
- - ✅ All request/response schemas validated
924
- - ✅ Authentication flow complete
925
- - ✅ Error handling verified
926
- - ✅ Performance benchmarks met
927
-
928
- ---
929
-
930
- ## Related Documentation
931
-
932
- - [Client Spec Compliance Matrix](./CLIENT_SPEC_COMPLIANCE.md)
933
- - [Client README](./README.md)
934
- - [Spec Protocol Map](../spec/PROTOCOL_MAP.md)
935
-
936
- ---
937
-
938
- **Last Updated:** 2026-02-09
939
- **Status:** 📝 Specification Complete - Ready for Implementation