@etus/bhono-app 0.1.6 โ 0.1.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/package.json +3 -2
- package/templates/base/.claude/commands/check-skill-rules.md +112 -29
- package/templates/base/.claude/commands/linear/implement-issue.md +383 -55
- package/templates/base/.claude/commands/ship.md +77 -13
- package/templates/base/.claude/hooks/package-lock.json +0 -419
- package/templates/base/.claude/hooks/skill-activation-prompt.ts +185 -113
- package/templates/base/.claude/hooks/skill-tool-guard.sh +6 -0
- package/templates/base/.claude/hooks/skill-tool-guard.ts +198 -0
- package/templates/base/.claude/scripts/validate-skill-rules.sh +55 -32
- package/templates/base/.claude/settings.json +18 -11
- package/templates/base/.claude/skills/skill-rules.json +326 -173
- package/templates/base/.env.example +3 -0
- package/templates/base/README.md +9 -7
- package/templates/base/config/eslint.config.js +1 -0
- package/templates/base/config/wrangler.json +16 -17
- package/templates/base/docs/SETUP-GUIDE.md +566 -0
- package/templates/base/docs/architecture/README.md +162 -8
- package/templates/base/docs/architecture/api-catalog.md +575 -0
- package/templates/base/docs/architecture/c4-component.md +309 -0
- package/templates/base/docs/architecture/c4-container.md +183 -0
- package/templates/base/docs/architecture/c4-context.md +106 -0
- package/templates/base/docs/architecture/dependencies.md +327 -0
- package/templates/base/docs/architecture/tech-debt.md +184 -0
- package/templates/base/package.json +20 -15
- package/templates/base/scripts/capture-prod-session.ts +2 -2
- package/templates/base/scripts/sync-template.sh +104 -0
- package/templates/base/src/server/db/sql.ts +24 -4
- package/templates/base/src/server/index.ts +1 -0
- package/templates/base/src/server/lib/audited-db.ts +10 -10
- package/templates/base/src/server/middleware/account.ts +1 -1
- package/templates/base/src/server/middleware/auth.ts +11 -11
- package/templates/base/src/server/middleware/rate-limit.ts +3 -6
- package/templates/base/src/server/routes/auth/handlers.ts +5 -5
- package/templates/base/src/server/routes/auth/test-login.ts +9 -9
- package/templates/base/src/server/routes/index.ts +9 -0
- package/templates/base/src/server/routes/invitations/handlers.ts +6 -6
- package/templates/base/src/server/routes/openapi.ts +1 -1
- package/templates/base/src/server/services/accounts.ts +9 -9
- package/templates/base/src/server/services/audits.ts +12 -12
- package/templates/base/src/server/services/auth.ts +15 -15
- package/templates/base/src/server/services/invitations.ts +16 -16
- package/templates/base/src/server/services/users.ts +13 -13
- package/templates/base/src/shared/types/api.ts +66 -198
- package/templates/base/tests/e2e/auth.setup.ts +1 -1
- package/templates/base/tests/unit/server/auth/guards.test.ts +1 -1
- package/templates/base/tests/unit/server/middleware/auth.test.ts +273 -0
- package/templates/base/tests/unit/server/routes/auth/handlers.test.ts +111 -0
- package/templates/base/tests/unit/server/routes/users/handlers.test.ts +69 -5
- package/templates/base/tests/unit/server/services/accounts.test.ts +148 -0
- package/templates/base/tests/unit/server/services/audits.test.ts +219 -0
- package/templates/base/tests/unit/server/services/auth.test.ts +480 -3
- package/templates/base/tests/unit/server/services/invitations.test.ts +178 -0
- package/templates/base/tests/unit/server/services/users.test.ts +363 -8
- package/templates/base/tests/unit/shared/schemas.test.ts +1 -1
- package/templates/base/vite.config.ts +3 -1
- package/templates/base/.github/workflows/test.yml +0 -127
- package/templates/base/.husky/pre-push +0 -26
- package/templates/base/auth-setup-error.png +0 -0
- package/templates/base/pnpm-lock.yaml +0 -8052
- package/templates/base/tests/e2e/_auth/.gitkeep +0 -0
- package/templates/base/tsconfig.tsbuildinfo +0 -1
|
@@ -18,6 +18,17 @@ vi.mock('@server/db/sql', () => ({
|
|
|
18
18
|
queryOne: vi.fn(),
|
|
19
19
|
queryAll: vi.fn(),
|
|
20
20
|
execute: vi.fn(),
|
|
21
|
+
toStringValue: (value: unknown) => {
|
|
22
|
+
if (typeof value === 'string') return value
|
|
23
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
24
|
+
return ''
|
|
25
|
+
},
|
|
26
|
+
toNullableString: (value: unknown) => {
|
|
27
|
+
if (value === null || value === undefined) return null
|
|
28
|
+
if (typeof value === 'string') return value
|
|
29
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
30
|
+
return null
|
|
31
|
+
},
|
|
21
32
|
}))
|
|
22
33
|
|
|
23
34
|
import { auditedUpdate, auditedDelete } from '@server/lib/audited-db'
|
|
@@ -82,7 +93,7 @@ describe('usersService', () => {
|
|
|
82
93
|
;(queryAll as Mock).mockResolvedValueOnce([
|
|
83
94
|
{
|
|
84
95
|
id: user.id,
|
|
85
|
-
google_id: user.
|
|
96
|
+
google_id: `google-${user.id}`,
|
|
86
97
|
email: user.email,
|
|
87
98
|
name: user.name,
|
|
88
99
|
avatar_url: user.avatarUrl,
|
|
@@ -115,7 +126,7 @@ describe('usersService', () => {
|
|
|
115
126
|
;(queryOne as Mock)
|
|
116
127
|
.mockResolvedValueOnce({
|
|
117
128
|
id: user.id,
|
|
118
|
-
google_id: user.
|
|
129
|
+
google_id: `google-${user.id}`,
|
|
119
130
|
email: user.email,
|
|
120
131
|
name: user.name,
|
|
121
132
|
avatar_url: user.avatarUrl,
|
|
@@ -138,7 +149,7 @@ describe('usersService', () => {
|
|
|
138
149
|
|
|
139
150
|
;(queryOne as Mock).mockResolvedValueOnce({
|
|
140
151
|
id: user.id,
|
|
141
|
-
google_id: user.
|
|
152
|
+
google_id: `google-${user.id}`,
|
|
142
153
|
email: user.email,
|
|
143
154
|
name: user.name,
|
|
144
155
|
avatar_url: user.avatarUrl,
|
|
@@ -178,7 +189,7 @@ describe('usersService', () => {
|
|
|
178
189
|
const user = createUserFixture({ id: 'user-1' })
|
|
179
190
|
;(queryOne as Mock).mockResolvedValueOnce({
|
|
180
191
|
id: user.id,
|
|
181
|
-
google_id: user.
|
|
192
|
+
google_id: `google-${user.id}`,
|
|
182
193
|
email: user.email,
|
|
183
194
|
name: user.name,
|
|
184
195
|
avatar_url: user.avatarUrl,
|
|
@@ -201,7 +212,7 @@ describe('usersService', () => {
|
|
|
201
212
|
const user = createUserFixture({ id: 'user-1', deletedAt: new Date().toISOString() })
|
|
202
213
|
;(queryOne as Mock).mockResolvedValueOnce({
|
|
203
214
|
id: user.id,
|
|
204
|
-
google_id: user.
|
|
215
|
+
google_id: `google-${user.id}`,
|
|
205
216
|
email: user.email,
|
|
206
217
|
name: user.name,
|
|
207
218
|
avatar_url: user.avatarUrl,
|
|
@@ -243,7 +254,7 @@ describe('usersService', () => {
|
|
|
243
254
|
;(queryOne as Mock)
|
|
244
255
|
.mockResolvedValueOnce({
|
|
245
256
|
id: user.id,
|
|
246
|
-
google_id: user.
|
|
257
|
+
google_id: `google-${user.id}`,
|
|
247
258
|
email: user.email,
|
|
248
259
|
name: user.name,
|
|
249
260
|
avatar_url: user.avatarUrl,
|
|
@@ -274,7 +285,7 @@ describe('usersService', () => {
|
|
|
274
285
|
;(queryOne as Mock)
|
|
275
286
|
.mockResolvedValueOnce({
|
|
276
287
|
id: user.id,
|
|
277
|
-
google_id: user.
|
|
288
|
+
google_id: `google-${user.id}`,
|
|
278
289
|
email: user.email,
|
|
279
290
|
name: user.name,
|
|
280
291
|
avatar_url: user.avatarUrl,
|
|
@@ -300,7 +311,7 @@ describe('usersService', () => {
|
|
|
300
311
|
|
|
301
312
|
;(queryOne as Mock).mockResolvedValueOnce({
|
|
302
313
|
id: user.id,
|
|
303
|
-
google_id: user.
|
|
314
|
+
google_id: `google-${user.id}`,
|
|
304
315
|
email: user.email,
|
|
305
316
|
name: user.name,
|
|
306
317
|
avatar_url: user.avatarUrl,
|
|
@@ -348,4 +359,348 @@ describe('usersService', () => {
|
|
|
348
359
|
expect(logAudit).toHaveBeenCalled()
|
|
349
360
|
})
|
|
350
361
|
})
|
|
362
|
+
|
|
363
|
+
describe('findAll edge cases', () => {
|
|
364
|
+
const defaultPagination: PaginationQuery = { page: 1, limit: 10 }
|
|
365
|
+
|
|
366
|
+
it('should filter by query parameter', async () => {
|
|
367
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
368
|
+
;(queryAll as Mock).mockResolvedValueOnce([
|
|
369
|
+
{
|
|
370
|
+
id: 'user-1',
|
|
371
|
+
google_id: 'google-1',
|
|
372
|
+
email: 'search@example.com',
|
|
373
|
+
name: 'Search User',
|
|
374
|
+
avatar_url: null,
|
|
375
|
+
status: 'active',
|
|
376
|
+
provider_ids: JSON.stringify(['google']),
|
|
377
|
+
is_super_admin: 0,
|
|
378
|
+
created_at: new Date().toISOString(),
|
|
379
|
+
updated_at: new Date().toISOString(),
|
|
380
|
+
deleted_at: null,
|
|
381
|
+
},
|
|
382
|
+
])
|
|
383
|
+
|
|
384
|
+
const result = await usersService.findAll(db, superAdminCtx, { ...defaultPagination, query: 'search' })
|
|
385
|
+
|
|
386
|
+
expect(result.data).toHaveLength(1)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('should filter by account for non-super-admin', async () => {
|
|
390
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
391
|
+
;(queryAll as Mock).mockResolvedValueOnce([
|
|
392
|
+
{
|
|
393
|
+
id: 'user-1',
|
|
394
|
+
google_id: 'google-1',
|
|
395
|
+
email: 'user@example.com',
|
|
396
|
+
name: 'User',
|
|
397
|
+
avatar_url: null,
|
|
398
|
+
status: 'active',
|
|
399
|
+
provider_ids: '[]',
|
|
400
|
+
is_super_admin: 0,
|
|
401
|
+
created_at: new Date().toISOString(),
|
|
402
|
+
updated_at: new Date().toISOString(),
|
|
403
|
+
deleted_at: null,
|
|
404
|
+
},
|
|
405
|
+
])
|
|
406
|
+
|
|
407
|
+
await usersService.findAll(db, ctx, defaultPagination)
|
|
408
|
+
|
|
409
|
+
// Check that account filter was applied
|
|
410
|
+
expect((queryOne as Mock).mock.calls[0][2]).toContain(ctx.accountId)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('should handle null count result', async () => {
|
|
414
|
+
;(queryOne as Mock).mockResolvedValueOnce(null)
|
|
415
|
+
;(queryAll as Mock).mockResolvedValueOnce([])
|
|
416
|
+
|
|
417
|
+
const result = await usersService.findAll(db, superAdminCtx, defaultPagination)
|
|
418
|
+
|
|
419
|
+
expect(result.data).toHaveLength(0)
|
|
420
|
+
expect(result.meta.totalItems).toBe(0)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('should parse providerIds as array', async () => {
|
|
424
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
425
|
+
;(queryAll as Mock).mockResolvedValueOnce([
|
|
426
|
+
{
|
|
427
|
+
id: 'user-1',
|
|
428
|
+
google_id: 'google-1',
|
|
429
|
+
email: 'user@example.com',
|
|
430
|
+
name: 'User',
|
|
431
|
+
avatar_url: null,
|
|
432
|
+
status: 'active',
|
|
433
|
+
provider_ids: ['google', 'github'],
|
|
434
|
+
is_super_admin: 0,
|
|
435
|
+
created_at: new Date().toISOString(),
|
|
436
|
+
updated_at: new Date().toISOString(),
|
|
437
|
+
deleted_at: null,
|
|
438
|
+
},
|
|
439
|
+
])
|
|
440
|
+
|
|
441
|
+
const result = await usersService.findAll(db, superAdminCtx, defaultPagination)
|
|
442
|
+
|
|
443
|
+
expect(result.data[0].providerIds).toEqual(['google', 'github'])
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('should handle invalid JSON in providerIds', async () => {
|
|
447
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
448
|
+
;(queryAll as Mock).mockResolvedValueOnce([
|
|
449
|
+
{
|
|
450
|
+
id: 'user-1',
|
|
451
|
+
google_id: 'google-1',
|
|
452
|
+
email: 'user@example.com',
|
|
453
|
+
name: 'User',
|
|
454
|
+
avatar_url: null,
|
|
455
|
+
status: 'active',
|
|
456
|
+
provider_ids: 'invalid-json',
|
|
457
|
+
is_super_admin: 0,
|
|
458
|
+
created_at: new Date().toISOString(),
|
|
459
|
+
updated_at: new Date().toISOString(),
|
|
460
|
+
deleted_at: null,
|
|
461
|
+
},
|
|
462
|
+
])
|
|
463
|
+
|
|
464
|
+
const result = await usersService.findAll(db, superAdminCtx, defaultPagination)
|
|
465
|
+
|
|
466
|
+
expect(result.data[0].providerIds).toEqual([])
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('should handle non-array JSON in providerIds', async () => {
|
|
470
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
471
|
+
;(queryAll as Mock).mockResolvedValueOnce([
|
|
472
|
+
{
|
|
473
|
+
id: 'user-1',
|
|
474
|
+
google_id: 'google-1',
|
|
475
|
+
email: 'user@example.com',
|
|
476
|
+
name: 'User',
|
|
477
|
+
avatar_url: null,
|
|
478
|
+
status: 'active',
|
|
479
|
+
provider_ids: '{"key": "value"}',
|
|
480
|
+
is_super_admin: 0,
|
|
481
|
+
created_at: new Date().toISOString(),
|
|
482
|
+
updated_at: new Date().toISOString(),
|
|
483
|
+
deleted_at: null,
|
|
484
|
+
},
|
|
485
|
+
])
|
|
486
|
+
|
|
487
|
+
const result = await usersService.findAll(db, superAdminCtx, defaultPagination)
|
|
488
|
+
|
|
489
|
+
expect(result.data[0].providerIds).toEqual([])
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('should handle isSuperAdmin as boolean true', async () => {
|
|
493
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
494
|
+
;(queryAll as Mock).mockResolvedValueOnce([
|
|
495
|
+
{
|
|
496
|
+
id: 'user-1',
|
|
497
|
+
google_id: 'google-1',
|
|
498
|
+
email: 'user@example.com',
|
|
499
|
+
name: 'User',
|
|
500
|
+
avatar_url: null,
|
|
501
|
+
status: 'active',
|
|
502
|
+
provider_ids: '[]',
|
|
503
|
+
is_super_admin: true,
|
|
504
|
+
created_at: new Date().toISOString(),
|
|
505
|
+
updated_at: new Date().toISOString(),
|
|
506
|
+
deleted_at: null,
|
|
507
|
+
},
|
|
508
|
+
])
|
|
509
|
+
|
|
510
|
+
const result = await usersService.findAll(db, superAdminCtx, defaultPagination)
|
|
511
|
+
|
|
512
|
+
expect(result.data[0].isSuperAdmin).toBe(true)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('should handle isSuperAdmin as string "1"', async () => {
|
|
516
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
517
|
+
;(queryAll as Mock).mockResolvedValueOnce([
|
|
518
|
+
{
|
|
519
|
+
id: 'user-1',
|
|
520
|
+
google_id: 'google-1',
|
|
521
|
+
email: 'user@example.com',
|
|
522
|
+
name: 'User',
|
|
523
|
+
avatar_url: null,
|
|
524
|
+
status: 'active',
|
|
525
|
+
provider_ids: '[]',
|
|
526
|
+
is_super_admin: '1',
|
|
527
|
+
created_at: new Date().toISOString(),
|
|
528
|
+
updated_at: new Date().toISOString(),
|
|
529
|
+
deleted_at: null,
|
|
530
|
+
},
|
|
531
|
+
])
|
|
532
|
+
|
|
533
|
+
const result = await usersService.findAll(db, superAdminCtx, defaultPagination)
|
|
534
|
+
|
|
535
|
+
expect(result.data[0].isSuperAdmin).toBe(true)
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('should handle isSuperAdmin as string "true"', async () => {
|
|
539
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
540
|
+
;(queryAll as Mock).mockResolvedValueOnce([
|
|
541
|
+
{
|
|
542
|
+
id: 'user-1',
|
|
543
|
+
google_id: 'google-1',
|
|
544
|
+
email: 'user@example.com',
|
|
545
|
+
name: 'User',
|
|
546
|
+
avatar_url: null,
|
|
547
|
+
status: 'active',
|
|
548
|
+
provider_ids: '[]',
|
|
549
|
+
is_super_admin: 'TRUE',
|
|
550
|
+
created_at: new Date().toISOString(),
|
|
551
|
+
updated_at: new Date().toISOString(),
|
|
552
|
+
deleted_at: null,
|
|
553
|
+
},
|
|
554
|
+
])
|
|
555
|
+
|
|
556
|
+
const result = await usersService.findAll(db, superAdminCtx, defaultPagination)
|
|
557
|
+
|
|
558
|
+
expect(result.data[0].isSuperAdmin).toBe(true)
|
|
559
|
+
})
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
describe('update edge cases', () => {
|
|
563
|
+
it('should throw when auditedUpdate returns empty array', async () => {
|
|
564
|
+
const user = createUserFixture({ id: 'user-1' })
|
|
565
|
+
|
|
566
|
+
;(queryOne as Mock).mockResolvedValueOnce({
|
|
567
|
+
id: user.id,
|
|
568
|
+
google_id: `google-${user.id}`,
|
|
569
|
+
email: user.email,
|
|
570
|
+
name: user.name,
|
|
571
|
+
avatar_url: user.avatarUrl,
|
|
572
|
+
status: user.status,
|
|
573
|
+
provider_ids: JSON.stringify(user.providerIds),
|
|
574
|
+
is_super_admin: user.isSuperAdmin ? 1 : 0,
|
|
575
|
+
created_at: user.createdAt,
|
|
576
|
+
updated_at: user.updatedAt,
|
|
577
|
+
deleted_at: user.deletedAt,
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
;(auditedUpdate as Mock).mockResolvedValueOnce([])
|
|
581
|
+
|
|
582
|
+
await expect(usersService.update(db, superAdminCtx, user.id, { name: 'New' })).rejects.toThrow('Failed to update user')
|
|
583
|
+
})
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
describe('restore edge cases', () => {
|
|
587
|
+
it('should throw NotFoundError when user not found', async () => {
|
|
588
|
+
;(queryOne as Mock).mockResolvedValueOnce(null)
|
|
589
|
+
|
|
590
|
+
await expect(usersService.restore(db, superAdminCtx, 'missing')).rejects.toThrow(NotFoundError)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('should throw NotFoundError when restore fails', async () => {
|
|
594
|
+
const user = createUserFixture({ id: 'user-1', deletedAt: new Date().toISOString() })
|
|
595
|
+
|
|
596
|
+
;(queryOne as Mock).mockResolvedValueOnce({
|
|
597
|
+
id: user.id,
|
|
598
|
+
google_id: `google-${user.id}`,
|
|
599
|
+
email: user.email,
|
|
600
|
+
name: user.name,
|
|
601
|
+
avatar_url: user.avatarUrl,
|
|
602
|
+
status: user.status,
|
|
603
|
+
provider_ids: JSON.stringify(user.providerIds),
|
|
604
|
+
is_super_admin: user.isSuperAdmin ? 1 : 0,
|
|
605
|
+
created_at: user.createdAt,
|
|
606
|
+
updated_at: user.updatedAt,
|
|
607
|
+
deleted_at: user.deletedAt,
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
;(auditedUpdate as Mock).mockResolvedValueOnce([])
|
|
611
|
+
|
|
612
|
+
await expect(usersService.restore(db, superAdminCtx, user.id)).rejects.toThrow(NotFoundError)
|
|
613
|
+
})
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
describe('updateRole edge cases', () => {
|
|
617
|
+
it('should throw NotFoundError when membership not found', async () => {
|
|
618
|
+
const user = createUserFixture({ id: 'user-1' })
|
|
619
|
+
|
|
620
|
+
;(queryOne as Mock)
|
|
621
|
+
.mockResolvedValueOnce({
|
|
622
|
+
id: user.id,
|
|
623
|
+
google_id: `google-${user.id}`,
|
|
624
|
+
email: user.email,
|
|
625
|
+
name: user.name,
|
|
626
|
+
avatar_url: user.avatarUrl,
|
|
627
|
+
status: user.status,
|
|
628
|
+
provider_ids: JSON.stringify(user.providerIds),
|
|
629
|
+
is_super_admin: user.isSuperAdmin ? 1 : 0,
|
|
630
|
+
created_at: user.createdAt,
|
|
631
|
+
updated_at: user.updatedAt,
|
|
632
|
+
deleted_at: user.deletedAt,
|
|
633
|
+
})
|
|
634
|
+
.mockResolvedValueOnce(null)
|
|
635
|
+
|
|
636
|
+
await expect(usersService.updateRole(db, superAdminCtx, user.id, 'account-1', 'ADMIN')).rejects.toThrow('User not found in account')
|
|
637
|
+
})
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
describe('createUserAccounts edge cases', () => {
|
|
641
|
+
it('should update existing membership', async () => {
|
|
642
|
+
const items = [{ userId: 'user-1', accountId: 'account-1', role: 'ADMIN' as const }]
|
|
643
|
+
|
|
644
|
+
;(queryOne as Mock).mockResolvedValueOnce({ role: 'VIEWER' })
|
|
645
|
+
|
|
646
|
+
const result = await usersService.createUserAccounts(db, superAdminCtx, items)
|
|
647
|
+
|
|
648
|
+
expect(result.count).toBe(1)
|
|
649
|
+
expect(execute).toHaveBeenCalled()
|
|
650
|
+
})
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
describe('listUserRoles edge cases', () => {
|
|
654
|
+
it('should handle snake_case column names', async () => {
|
|
655
|
+
const user = createUserFixture({ id: 'user-1' })
|
|
656
|
+
|
|
657
|
+
;(queryOne as Mock)
|
|
658
|
+
.mockResolvedValueOnce({
|
|
659
|
+
id: user.id,
|
|
660
|
+
google_id: `google-${user.id}`,
|
|
661
|
+
email: user.email,
|
|
662
|
+
name: user.name,
|
|
663
|
+
avatar_url: user.avatarUrl,
|
|
664
|
+
status: user.status,
|
|
665
|
+
provider_ids: JSON.stringify(user.providerIds),
|
|
666
|
+
is_super_admin: user.isSuperAdmin ? 1 : 0,
|
|
667
|
+
created_at: user.createdAt,
|
|
668
|
+
updated_at: user.updatedAt,
|
|
669
|
+
deleted_at: user.deletedAt,
|
|
670
|
+
})
|
|
671
|
+
.mockResolvedValueOnce({ ok: 1 })
|
|
672
|
+
|
|
673
|
+
;(queryAll as Mock).mockResolvedValueOnce([{ account_id: 'account-1', role: 'ADMIN' }])
|
|
674
|
+
|
|
675
|
+
const result = await usersService.listUserRoles(db, ctx, user.id)
|
|
676
|
+
|
|
677
|
+
expect(result[0].accountId).toBe('account-1')
|
|
678
|
+
})
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
describe('findById edge cases', () => {
|
|
682
|
+
it('should return user for super admin without membership check', async () => {
|
|
683
|
+
const user = createUserFixture({ id: 'user-1' })
|
|
684
|
+
|
|
685
|
+
;(queryOne as Mock).mockResolvedValueOnce({
|
|
686
|
+
id: user.id,
|
|
687
|
+
googleId: user.googleId,
|
|
688
|
+
email: user.email,
|
|
689
|
+
name: user.name,
|
|
690
|
+
avatarUrl: user.avatarUrl,
|
|
691
|
+
status: user.status,
|
|
692
|
+
providerIds: user.providerIds,
|
|
693
|
+
isSuperAdmin: user.isSuperAdmin,
|
|
694
|
+
createdAt: user.createdAt,
|
|
695
|
+
updatedAt: user.updatedAt,
|
|
696
|
+
deletedAt: user.deletedAt,
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
const result = await usersService.findById(db, superAdminCtx, user.id)
|
|
700
|
+
|
|
701
|
+
expect(result.id).toBe(user.id)
|
|
702
|
+
// queryOne should only be called once (no membership check)
|
|
703
|
+
expect(queryOne).toHaveBeenCalledTimes(1)
|
|
704
|
+
})
|
|
705
|
+
})
|
|
351
706
|
})
|
|
@@ -18,7 +18,9 @@ export default defineConfig({
|
|
|
18
18
|
generatedRouteTree: "./src/client/routeTree.gen.ts",
|
|
19
19
|
}),
|
|
20
20
|
react(),
|
|
21
|
-
cloudflare(
|
|
21
|
+
cloudflare({
|
|
22
|
+
configPath: "./config/wrangler.json",
|
|
23
|
+
}),
|
|
22
24
|
// Istanbul instrumentation for E2E coverage
|
|
23
25
|
// Only add plugin when E2E_COVERAGE is true, then it instruments without additional env check
|
|
24
26
|
...(isE2ECoverage
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
name: Test Suite
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main, master]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main, master]
|
|
8
|
-
|
|
9
|
-
env:
|
|
10
|
-
CI: true
|
|
11
|
-
NODE_VERSION: '20'
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
# Unit and Integration Tests (Vitest) with Coverage
|
|
15
|
-
unit-tests:
|
|
16
|
-
name: Unit Tests
|
|
17
|
-
runs-on: ubuntu-latest
|
|
18
|
-
|
|
19
|
-
steps:
|
|
20
|
-
- name: Checkout code
|
|
21
|
-
uses: actions/checkout@v4
|
|
22
|
-
|
|
23
|
-
- name: Setup Node.js
|
|
24
|
-
uses: actions/setup-node@v4
|
|
25
|
-
with:
|
|
26
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
27
|
-
cache: 'npm'
|
|
28
|
-
|
|
29
|
-
- name: Install dependencies
|
|
30
|
-
run: npm ci
|
|
31
|
-
|
|
32
|
-
- name: Run backend tests with coverage
|
|
33
|
-
run: npm run test:coverage
|
|
34
|
-
|
|
35
|
-
- name: Run frontend tests with coverage
|
|
36
|
-
run: npm run test:coverage:client
|
|
37
|
-
|
|
38
|
-
- name: Upload coverage reports
|
|
39
|
-
uses: codecov/codecov-action@v4
|
|
40
|
-
if: always()
|
|
41
|
-
with:
|
|
42
|
-
files: ./coverage/server/lcov.info,./coverage/client/lcov.info
|
|
43
|
-
flags: unittests
|
|
44
|
-
fail_ci_if_error: false
|
|
45
|
-
|
|
46
|
-
# E2E Tests (Playwright)
|
|
47
|
-
e2e-tests:
|
|
48
|
-
name: E2E Tests
|
|
49
|
-
runs-on: ubuntu-latest
|
|
50
|
-
timeout-minutes: 30
|
|
51
|
-
|
|
52
|
-
steps:
|
|
53
|
-
- name: Checkout code
|
|
54
|
-
uses: actions/checkout@v4
|
|
55
|
-
|
|
56
|
-
- name: Setup Node.js
|
|
57
|
-
uses: actions/setup-node@v4
|
|
58
|
-
with:
|
|
59
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
60
|
-
cache: 'npm'
|
|
61
|
-
|
|
62
|
-
- name: Install dependencies
|
|
63
|
-
run: npm ci
|
|
64
|
-
|
|
65
|
-
- name: Install Playwright browsers
|
|
66
|
-
run: npx playwright install chromium --with-deps
|
|
67
|
-
|
|
68
|
-
- name: Run E2E tests
|
|
69
|
-
run: npm run test:e2e -- --project=chromium-unauth
|
|
70
|
-
|
|
71
|
-
- name: Upload Playwright report
|
|
72
|
-
uses: actions/upload-artifact@v4
|
|
73
|
-
if: always()
|
|
74
|
-
with:
|
|
75
|
-
name: playwright-report
|
|
76
|
-
path: playwright-report/
|
|
77
|
-
retention-days: 7
|
|
78
|
-
|
|
79
|
-
- name: Upload test results
|
|
80
|
-
uses: actions/upload-artifact@v4
|
|
81
|
-
if: failure()
|
|
82
|
-
with:
|
|
83
|
-
name: test-results
|
|
84
|
-
path: test-results/
|
|
85
|
-
retention-days: 7
|
|
86
|
-
|
|
87
|
-
# Type checking
|
|
88
|
-
typecheck:
|
|
89
|
-
name: Type Check
|
|
90
|
-
runs-on: ubuntu-latest
|
|
91
|
-
|
|
92
|
-
steps:
|
|
93
|
-
- name: Checkout code
|
|
94
|
-
uses: actions/checkout@v4
|
|
95
|
-
|
|
96
|
-
- name: Setup Node.js
|
|
97
|
-
uses: actions/setup-node@v4
|
|
98
|
-
with:
|
|
99
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
100
|
-
cache: 'npm'
|
|
101
|
-
|
|
102
|
-
- name: Install dependencies
|
|
103
|
-
run: npm ci
|
|
104
|
-
|
|
105
|
-
- name: Run type check
|
|
106
|
-
run: npx tsc --noEmit
|
|
107
|
-
|
|
108
|
-
# Linting
|
|
109
|
-
lint:
|
|
110
|
-
name: Lint
|
|
111
|
-
runs-on: ubuntu-latest
|
|
112
|
-
|
|
113
|
-
steps:
|
|
114
|
-
- name: Checkout code
|
|
115
|
-
uses: actions/checkout@v4
|
|
116
|
-
|
|
117
|
-
- name: Setup Node.js
|
|
118
|
-
uses: actions/setup-node@v4
|
|
119
|
-
with:
|
|
120
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
121
|
-
cache: 'npm'
|
|
122
|
-
|
|
123
|
-
- name: Install dependencies
|
|
124
|
-
run: npm ci
|
|
125
|
-
|
|
126
|
-
- name: Run linter
|
|
127
|
-
run: npm run lint
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env sh
|
|
2
|
-
|
|
3
|
-
echo "๐ Running pre-push checks..."
|
|
4
|
-
|
|
5
|
-
# Type checking
|
|
6
|
-
echo "๐ Type checking..."
|
|
7
|
-
pnpm typecheck || {
|
|
8
|
-
echo "โ Type check failed. Push aborted."
|
|
9
|
-
exit 1
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
# Server unit tests only (faster, more reliable)
|
|
13
|
-
echo "๐งช Running server unit tests..."
|
|
14
|
-
pnpm test:unit:server || {
|
|
15
|
-
echo "โ Server unit tests failed. Push aborted."
|
|
16
|
-
exit 1
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
# Build verification
|
|
20
|
-
echo "๐๏ธ Verifying build..."
|
|
21
|
-
pnpm build || {
|
|
22
|
-
echo "โ Build failed. Push aborted."
|
|
23
|
-
exit 1
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
echo "โ
All pre-push checks passed!"
|
|
Binary file
|