create-fullstack-scaffold 0.4.9 → 0.4.11

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.
Files changed (139) hide show
  1. package/dist/cli/index.js +499 -100
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/template/CLAUDE.md +18 -6
  5. package/template/drizzle/0004_add_merchants_products.sql +30 -0
  6. package/template/drizzle/meta/_journal.json +7 -0
  7. package/template/eslint-rules/module-boundary.js +6 -2
  8. package/template/eslint-rules/no-cross-module-service-import.js +105 -0
  9. package/template/eslint-rules/no-disable-type-safe-client.js +5 -0
  10. package/template/eslint.config.js +9 -0
  11. package/template/lint-scripts/config/project.config.ts +21 -0
  12. package/template/lint-scripts/validate-all.ts +48 -12
  13. package/template/lint-scripts/validators/index.ts +29 -0
  14. package/template/lint-scripts/validators/module-public-api.validator.ts +103 -0
  15. package/template/lint-scripts/validators/schema-uniqueness.validator.ts +147 -0
  16. package/template/modules.config.ts +2 -0
  17. package/template/package.json +2 -0
  18. package/template/playwright.config.ts +14 -0
  19. package/template/src/admin/App.tsx +40 -3
  20. package/template/src/admin/components/LanguageSwitcher.tsx +22 -0
  21. package/template/src/admin/components/NotificationDrawer.tsx +108 -38
  22. package/template/src/admin/components/StatsCard.tsx +3 -1
  23. package/template/src/admin/components/ThemeToggle.tsx +28 -0
  24. package/template/src/admin/hooks/useRoles.ts +2 -2
  25. package/template/src/admin/i18n/index.ts +18 -0
  26. package/template/src/admin/i18n/locales/en-US.json +381 -0
  27. package/template/src/admin/i18n/locales/zh-CN.json +381 -0
  28. package/template/src/admin/i18n/useLanguage.ts +21 -0
  29. package/template/src/admin/layouts/Header.tsx +37 -16
  30. package/template/src/admin/layouts/Layout.tsx +7 -3
  31. package/template/src/admin/layouts/Sidebar.tsx +70 -68
  32. package/template/src/admin/main.tsx +1 -0
  33. package/template/src/admin/pages/ContentPage.tsx +68 -56
  34. package/template/src/admin/pages/DashboardPage.tsx +82 -76
  35. package/template/src/admin/pages/DisputesPage.tsx +61 -53
  36. package/template/src/admin/pages/LoginPage.tsx +41 -33
  37. package/template/src/admin/pages/OrdersPage.tsx +69 -64
  38. package/template/src/admin/pages/PermissionsPage.tsx +10 -8
  39. package/template/src/admin/pages/PluginDashboardPage.tsx +3 -1
  40. package/template/src/admin/pages/PluginManagementPage.tsx +3 -1
  41. package/template/src/admin/pages/RegisterPage.tsx +18 -16
  42. package/template/src/admin/pages/RolesPage.tsx +51 -49
  43. package/template/src/admin/pages/SettingsPage.tsx +22 -22
  44. package/template/src/admin/pages/SystemLogsPage.tsx +32 -30
  45. package/template/src/admin/pages/TicketsPage.tsx +67 -59
  46. package/template/src/admin/pages/UsersPage.tsx +63 -53
  47. package/template/src/admin/pages/__tests__/RolesPage.test.tsx +2 -2
  48. package/template/src/admin/stores/themeStore.ts +32 -0
  49. package/template/src/cli/modules/admin/index.ts +200 -0
  50. package/template/src/cli/modules/captcha/index.ts +40 -0
  51. package/template/src/cli/modules/chat/index.ts +21 -0
  52. package/template/src/cli/modules/content/index.ts +144 -0
  53. package/template/src/cli/modules/dispute/index.ts +150 -0
  54. package/template/src/cli/modules/file/index.ts +55 -0
  55. package/template/src/cli/modules/index.ts +30 -5
  56. package/template/src/cli/modules/order/index.ts +170 -0
  57. package/template/src/cli/modules/permission/index.ts +195 -0
  58. package/template/src/cli/modules/tenant/index.ts +142 -0
  59. package/template/src/cli/modules/ticket/index.ts +167 -0
  60. package/template/src/cli/rpc/client.ts +2 -2
  61. package/template/src/client/index.css +73 -0
  62. package/template/src/merchant/App.tsx +2 -0
  63. package/template/src/merchant/components/MerchantGuard.tsx +2 -6
  64. package/template/src/merchant/components/__tests__/MerchantGuard.test.tsx +117 -0
  65. package/template/src/merchant/layouts/Header.tsx +2 -2
  66. package/template/src/merchant/pages/DashboardPage.tsx +2 -2
  67. package/template/src/merchant/pages/DisputesPage.tsx +14 -10
  68. package/template/src/merchant/pages/LoginPage.tsx +49 -0
  69. package/template/src/merchant/pages/OrdersPage.tsx +17 -10
  70. package/template/src/merchant/pages/ProductsPage.tsx +31 -8
  71. package/template/src/merchant/pages/SettingsPage.tsx +3 -7
  72. package/template/src/merchant/stores/__tests__/merchantStore.test.ts +317 -0
  73. package/template/src/merchant/stores/merchantStore.ts +24 -27
  74. package/template/src/server/db/schema/index.ts +2 -0
  75. package/template/src/server/db/schema/merchants.ts +26 -0
  76. package/template/src/server/db/schema/products.ts +25 -0
  77. package/template/src/server/db/test-setup.ts +237 -0
  78. package/template/src/server/middleware/__tests__/audit-log.test.ts +144 -0
  79. package/template/src/server/middleware/__tests__/cors.test.ts +84 -0
  80. package/template/src/server/middleware/__tests__/logger.test.ts +117 -0
  81. package/template/src/server/middleware/__tests__/rate-limit.test.ts +73 -0
  82. package/template/src/server/middleware/__tests__/realtime-env.test.ts +56 -0
  83. package/template/src/server/middleware/__tests__/tenant-isolation.test.ts +150 -0
  84. package/template/src/server/module-admin/__tests__/admin-routes.test.ts +1 -1
  85. package/template/src/server/module-admin/module.ts +4 -0
  86. package/template/src/server/module-admin/routes/admin-notification-routes.ts +3 -3
  87. package/template/src/server/module-admin/routes/user-management-routes.ts +2 -2
  88. package/template/src/server/module-auth/__tests__/auth-routes.test.ts +315 -0
  89. package/template/src/server/module-auth/__tests__/profile-routes.test.ts +83 -0
  90. package/template/src/server/module-auth/module.ts +2 -0
  91. package/template/src/server/module-content/module.ts +1 -0
  92. package/template/src/server/module-content/routes/content-routes.ts +2 -2
  93. package/template/src/server/module-dispute/module.ts +1 -0
  94. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -2
  95. package/template/src/server/module-merchant/__tests__/merchant-routes.test.ts +218 -0
  96. package/template/src/server/module-merchant/__tests__/merchant-service.test.ts +183 -0
  97. package/template/src/server/module-merchant/index.ts +10 -0
  98. package/template/src/server/module-merchant/module.ts +33 -0
  99. package/template/src/server/module-merchant/routes/merchant-routes.ts +138 -0
  100. package/template/src/server/module-merchant/services/merchant-service.ts +292 -0
  101. package/template/src/server/module-notifications/index.ts +18 -0
  102. package/template/src/server/module-notifications/module.ts +2 -0
  103. package/template/src/server/module-order/module.ts +1 -0
  104. package/template/src/server/module-order/routes/order-routes.ts +2 -2
  105. package/template/src/server/module-permission/routes/role-routes.ts +3 -3
  106. package/template/src/server/module-plugin/__tests__/admin-category-service.test.ts +181 -0
  107. package/template/src/server/module-plugin/__tests__/admin-plugin-service.test.ts +277 -0
  108. package/template/src/server/module-plugin/__tests__/admin-stats-service.test.ts +170 -0
  109. package/template/src/server/module-plugin/__tests__/plugin-review-service.test.ts +260 -0
  110. package/template/src/server/module-plugin/__tests__/plugin-seed-service.test.ts +170 -0
  111. package/template/src/server/module-plugin/module.ts +3 -0
  112. package/template/src/server/module-tenant/__tests__/tenant-routes.test.ts +142 -0
  113. package/template/src/server/module-tenant/__tests__/tenant-service.test.ts +152 -0
  114. package/template/src/server/module-tenant/module.ts +1 -0
  115. package/template/src/server/module-tenant/services/tenant-service.ts +1 -37
  116. package/template/src/server/module-ticket/module.ts +1 -0
  117. package/template/src/server/module-ticket/routes/ticket-routes.ts +2 -2
  118. package/template/src/server/module-todos/module.ts +3 -0
  119. package/template/src/server/route-registry.ts +4 -0
  120. package/template/src/server/utils/__tests__/date.test.ts +130 -0
  121. package/template/src/server/utils/__tests__/env.test.ts +25 -0
  122. package/template/src/server/utils/__tests__/generate.test.ts +82 -0
  123. package/template/src/server/utils/__tests__/id-helpers.test.ts +29 -0
  124. package/template/src/server/utils/__tests__/json.test.ts +49 -0
  125. package/template/src/server/utils/__tests__/permission-utils.test.ts +26 -0
  126. package/template/src/server/utils/__tests__/uuid.test.ts +25 -0
  127. package/template/src/shared/core/module-manifest.ts +15 -0
  128. package/template/src/shared/modules/admin/schemas.ts +1 -1
  129. package/template/src/shared/modules/content/schemas.ts +2 -2
  130. package/template/src/shared/modules/dispute/schemas.ts +2 -2
  131. package/template/src/shared/modules/index.ts +181 -1
  132. package/template/src/shared/modules/merchant/index.ts +12 -0
  133. package/template/src/shared/modules/merchant/schemas.ts +63 -0
  134. package/template/src/shared/modules/order/schemas.ts +2 -2
  135. package/template/src/shared/modules/permission/index.ts +1 -1
  136. package/template/src/shared/modules/permission/schemas.ts +1 -1
  137. package/template/src/shared/modules/role/schemas.ts +2 -2
  138. package/template/src/shared/modules/ticket/schemas.ts +2 -2
  139. package/template/src/shared/schemas/index.ts +74 -2
@@ -0,0 +1,260 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'
2
+ import * as reviewService from '../services/plugin-review-service'
3
+ import { getRawClient, getDb } from '@server/db'
4
+ import { setupTestDatabase, cleanupTestDatabase } from '@server/db/test-setup'
5
+ import { NotFoundError, AuthorizationError, ConflictError } from '@server/utils/app-error'
6
+
7
+ async function clearPluginTables() {
8
+ const client = await getRawClient()
9
+ if (client && 'execute' in client) {
10
+ await client.execute('DELETE FROM plugin_reviews')
11
+ await client.execute('DELETE FROM plugin_versions')
12
+ await client.execute('DELETE FROM plugin_category_mappings')
13
+ await client.execute('DELETE FROM plugins')
14
+ }
15
+ }
16
+
17
+ async function insertTestPlugin(overrides: Record<string, unknown> = {}) {
18
+ const client = await getRawClient()
19
+ if (!client || !('execute' in client)) throw new Error('No DB client')
20
+
21
+ const id = overrides.id ?? 'test-plugin-id'
22
+ const now = Date.now()
23
+ await client.execute({
24
+ sql: `INSERT INTO plugins (id, name, slug, description, author_id, author_name, version, status, download_count, view_count, featured, created_at, updated_at)
25
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
26
+ args: [
27
+ id as string,
28
+ (overrides.name ?? 'Test Plugin') as string,
29
+ (overrides.slug ?? 'test-plugin') as string,
30
+ (overrides.description ?? 'A test plugin') as string,
31
+ (overrides.authorId ?? 'user-1') as string,
32
+ (overrides.authorName ?? 'Test User') as string,
33
+ (overrides.version ?? '0.0.1') as string,
34
+ (overrides.status ?? 'approved') as string,
35
+ (overrides.downloadCount ?? 0) as number,
36
+ (overrides.viewCount ?? 0) as number,
37
+ (overrides.featured === true ? 1 : 0) as number,
38
+ (overrides.createdAt ?? now) as number,
39
+ (overrides.updatedAt ?? now) as number,
40
+ ],
41
+ })
42
+ return id
43
+ }
44
+
45
+ async function insertTestReview(overrides: Record<string, unknown> = {}) {
46
+ const client = await getRawClient()
47
+ if (!client || !('execute' in client)) throw new Error('No DB client')
48
+
49
+ const id = overrides.id ?? 'test-review-id'
50
+ const now = Date.now()
51
+ await client.execute({
52
+ sql: `INSERT INTO plugin_reviews (id, plugin_id, user_id, user_name, rating, title, content, created_at)
53
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
54
+ args: [
55
+ id as string,
56
+ (overrides.pluginId ?? 'test-plugin-id') as string,
57
+ (overrides.userId ?? 'user-1') as string,
58
+ (overrides.userName ?? 'Reviewer') as string,
59
+ (overrides.rating ?? 5) as number,
60
+ (overrides.title ?? null) as string | null,
61
+ (overrides.content ?? null) as string | null,
62
+ (overrides.createdAt ?? now) as number,
63
+ ],
64
+ })
65
+ return id
66
+ }
67
+
68
+ describe('Plugin Review Service', () => {
69
+ beforeAll(async () => {
70
+ await setupTestDatabase()
71
+ const db = await getDb()
72
+ expect(db).toBeDefined()
73
+ })
74
+
75
+ afterAll(async () => {
76
+ await cleanupTestDatabase()
77
+ })
78
+
79
+ beforeEach(async () => {
80
+ await clearPluginTables()
81
+ })
82
+
83
+ afterEach(async () => {
84
+ await clearPluginTables()
85
+ vi.clearAllMocks()
86
+ })
87
+
88
+ describe('submitReview', () => {
89
+ it('should submit a review for an existing plugin', async () => {
90
+ await insertTestPlugin({ id: 'plugin-1', slug: 'reviewed-plugin' })
91
+
92
+ const review = await reviewService.submitReview({
93
+ pluginId: 'plugin-1',
94
+ userId: 'user-1',
95
+ userName: 'Alice',
96
+ rating: 4,
97
+ title: 'Great plugin',
98
+ content: 'Very useful',
99
+ })
100
+
101
+ expect(review.id).toBeDefined()
102
+ expect(review.pluginId).toBe('plugin-1')
103
+ expect(review.userId).toBe('user-1')
104
+ expect(review.userName).toBe('Alice')
105
+ expect(review.rating).toBe(4)
106
+ expect(review.title).toBe('Great plugin')
107
+ expect(review.content).toBe('Very useful')
108
+ expect(review.createdAt).toBeDefined()
109
+ })
110
+
111
+ it('should submit a review without optional fields', async () => {
112
+ await insertTestPlugin({ id: 'plugin-2', slug: 'minimal-review' })
113
+
114
+ const review = await reviewService.submitReview({
115
+ pluginId: 'plugin-2',
116
+ userId: 'user-1',
117
+ userName: 'Bob',
118
+ rating: 3,
119
+ })
120
+
121
+ expect(review.rating).toBe(3)
122
+ expect(review.title).toBeUndefined()
123
+ expect(review.content).toBeUndefined()
124
+ })
125
+
126
+ it('should throw NotFoundError for non-existent plugin', async () => {
127
+ await expect(
128
+ reviewService.submitReview({
129
+ pluginId: 'non-existent',
130
+ userId: 'user-1',
131
+ userName: 'Alice',
132
+ rating: 5,
133
+ })
134
+ ).rejects.toThrow(NotFoundError)
135
+ })
136
+
137
+ it('should throw ConflictError when user already reviewed', async () => {
138
+ await insertTestPlugin({ id: 'plugin-3', slug: 'dup-review-plugin' })
139
+ await insertTestReview({
140
+ id: 'existing-review',
141
+ pluginId: 'plugin-3',
142
+ userId: 'user-1',
143
+ })
144
+
145
+ await expect(
146
+ reviewService.submitReview({
147
+ pluginId: 'plugin-3',
148
+ userId: 'user-1',
149
+ userName: 'Alice',
150
+ rating: 5,
151
+ })
152
+ ).rejects.toThrow(ConflictError)
153
+ })
154
+
155
+ it('should allow different users to review the same plugin', async () => {
156
+ await insertTestPlugin({ id: 'plugin-4', slug: 'multi-review' })
157
+ await insertTestReview({
158
+ id: 'review-user1',
159
+ pluginId: 'plugin-4',
160
+ userId: 'user-1',
161
+ })
162
+
163
+ const review = await reviewService.submitReview({
164
+ pluginId: 'plugin-4',
165
+ userId: 'user-2',
166
+ userName: 'Bob',
167
+ rating: 2,
168
+ })
169
+
170
+ expect(review.userId).toBe('user-2')
171
+ expect(review.rating).toBe(2)
172
+ })
173
+ })
174
+
175
+ describe('getReviews', () => {
176
+ it('should return reviews for a plugin', async () => {
177
+ await insertTestPlugin({ id: 'plugin-5', slug: 'get-reviews' })
178
+ await insertTestReview({ id: 'r1', pluginId: 'plugin-5', userId: 'u1', userName: 'A', rating: 5 })
179
+ await insertTestReview({ id: 'r2', pluginId: 'plugin-5', userId: 'u2', userName: 'B', rating: 3 })
180
+
181
+ const reviews = await reviewService.getReviews('plugin-5')
182
+
183
+ expect(reviews).toHaveLength(2)
184
+ })
185
+
186
+ it('should return empty array for plugin with no reviews', async () => {
187
+ await insertTestPlugin({ id: 'plugin-6', slug: 'no-reviews' })
188
+
189
+ const reviews = await reviewService.getReviews('plugin-6')
190
+
191
+ expect(reviews).toEqual([])
192
+ })
193
+
194
+ it('should only return reviews for the specified plugin', async () => {
195
+ await insertTestPlugin({ id: 'plugin-7a', slug: 'review-target-a' })
196
+ await insertTestPlugin({ id: 'plugin-7b', slug: 'review-target-b' })
197
+ await insertTestReview({ id: 'r3', pluginId: 'plugin-7a', userId: 'u1', rating: 5 })
198
+ await insertTestReview({ id: 'r4', pluginId: 'plugin-7b', userId: 'u2', rating: 1 })
199
+
200
+ const reviews = await reviewService.getReviews('plugin-7a')
201
+
202
+ expect(reviews).toHaveLength(1)
203
+ expect(reviews[0].pluginId).toBe('plugin-7a')
204
+ })
205
+ })
206
+
207
+ describe('deleteReview', () => {
208
+ it('should delete a review by its author', async () => {
209
+ await insertTestPlugin({ id: 'plugin-8', slug: 'delete-review' })
210
+ await insertTestReview({ id: 'r-del', pluginId: 'plugin-8', userId: 'user-1' })
211
+
212
+ await reviewService.deleteReview('r-del', 'user-1')
213
+
214
+ const reviews = await reviewService.getReviews('plugin-8')
215
+ expect(reviews).toHaveLength(0)
216
+ })
217
+
218
+ it('should throw NotFoundError for non-existent review', async () => {
219
+ await expect(reviewService.deleteReview('non-existent', 'user-1')).rejects.toThrow(
220
+ NotFoundError
221
+ )
222
+ })
223
+
224
+ it('should throw AuthorizationError when non-author tries to delete', async () => {
225
+ await insertTestPlugin({ id: 'plugin-9', slug: 'auth-delete' })
226
+ await insertTestReview({ id: 'r-auth', pluginId: 'plugin-9', userId: 'user-1' })
227
+
228
+ await expect(reviewService.deleteReview('r-auth', 'user-2')).rejects.toThrow(
229
+ AuthorizationError
230
+ )
231
+ })
232
+ })
233
+
234
+ describe('getReviewById', () => {
235
+ it('should return a review by id', async () => {
236
+ await insertTestPlugin({ id: 'plugin-10', slug: 'get-by-id' })
237
+ await insertTestReview({
238
+ id: 'r-fetch',
239
+ pluginId: 'plugin-10',
240
+ userId: 'user-1',
241
+ userName: 'Alice',
242
+ rating: 5,
243
+ title: 'Excellent',
244
+ })
245
+
246
+ const review = await reviewService.getReviewById('r-fetch')
247
+
248
+ expect(review).not.toBeNull()
249
+ expect(review!.id).toBe('r-fetch')
250
+ expect(review!.rating).toBe(5)
251
+ expect(review!.title).toBe('Excellent')
252
+ })
253
+
254
+ it('should return null for non-existent review', async () => {
255
+ const review = await reviewService.getReviewById('non-existent')
256
+
257
+ expect(review).toBeNull()
258
+ })
259
+ })
260
+ })
@@ -0,0 +1,170 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'
2
+ import { seedPluginsIfEmpty } from '../services/plugin-seed-service'
3
+ import { getRawClient, getDb } from '@server/db'
4
+ import { setupTestDatabase, cleanupTestDatabase } from '@server/db/test-setup'
5
+
6
+ async function clearPluginTables() {
7
+ const client = await getRawClient()
8
+ if (client && 'execute' in client) {
9
+ await client.execute('DELETE FROM plugin_category_mappings')
10
+ await client.execute('DELETE FROM plugin_reviews')
11
+ await client.execute('DELETE FROM plugin_versions')
12
+ await client.execute('DELETE FROM plugins')
13
+ await client.execute('DELETE FROM plugin_categories')
14
+ }
15
+ }
16
+
17
+ describe('Plugin Seed Service', () => {
18
+ beforeAll(async () => {
19
+ await setupTestDatabase()
20
+ const db = await getDb()
21
+ expect(db).toBeDefined()
22
+ })
23
+
24
+ afterAll(async () => {
25
+ await cleanupTestDatabase()
26
+ })
27
+
28
+ beforeEach(async () => {
29
+ await clearPluginTables()
30
+ })
31
+
32
+ afterEach(async () => {
33
+ await clearPluginTables()
34
+ vi.clearAllMocks()
35
+ })
36
+
37
+ describe('seedPluginsIfEmpty', () => {
38
+ it('should seed sample data when database is empty', async () => {
39
+ await seedPluginsIfEmpty()
40
+
41
+ const client = await getRawClient()
42
+ if (client && 'execute' in client) {
43
+ const pluginResult = await client.execute('SELECT COUNT(*) as count FROM plugins')
44
+ const pluginCount = (pluginResult.rows[0] as unknown as { count: number })?.count ?? 0
45
+ expect(pluginCount).toBeGreaterThan(0)
46
+
47
+ const categoryResult = await client.execute('SELECT COUNT(*) as count FROM plugin_categories')
48
+ const categoryCount = (categoryResult.rows[0] as unknown as { count: number })?.count ?? 0
49
+ expect(categoryCount).toBeGreaterThan(0)
50
+
51
+ const versionResult = await client.execute('SELECT COUNT(*) as count FROM plugin_versions')
52
+ const versionCount = (versionResult.rows[0] as unknown as { count: number })?.count ?? 0
53
+ expect(versionCount).toBeGreaterThan(0)
54
+
55
+ const mappingResult = await client.execute(
56
+ 'SELECT COUNT(*) as count FROM plugin_category_mappings'
57
+ )
58
+ const mappingCount = (mappingResult.rows[0] as unknown as { count: number })?.count ?? 0
59
+ expect(mappingCount).toBeGreaterThan(0)
60
+ }
61
+ })
62
+
63
+ it('should not seed when plugins already exist', async () => {
64
+ const client = await getRawClient()
65
+ if (client && 'execute' in client) {
66
+ await client.execute({
67
+ sql: `INSERT INTO plugins (id, name, slug, description, author_id, author_name, version, status, download_count, view_count, featured, created_at, updated_at)
68
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
69
+ args: [
70
+ 'existing-plugin',
71
+ 'Existing',
72
+ 'existing-plugin',
73
+ 'Already here',
74
+ 'user-1',
75
+ 'Dev',
76
+ '1.0.0',
77
+ 'approved',
78
+ 0,
79
+ 0,
80
+ 0,
81
+ Date.now(),
82
+ Date.now(),
83
+ ],
84
+ })
85
+ }
86
+
87
+ await seedPluginsIfEmpty()
88
+
89
+ if (client && 'execute' in client) {
90
+ const result = await client.execute('SELECT COUNT(*) as count FROM plugins')
91
+ const count = (result.rows[0] as unknown as { count: number })?.count ?? 0
92
+ expect(count).toBe(1)
93
+ }
94
+ })
95
+
96
+ it('should seed correct number of sample plugins', async () => {
97
+ await seedPluginsIfEmpty()
98
+
99
+ const client = await getRawClient()
100
+ if (client && 'execute' in client) {
101
+ const result = await client.execute('SELECT COUNT(*) as count FROM plugins')
102
+ const count = (result.rows[0] as unknown as { count: number })?.count ?? 0
103
+ expect(count).toBe(25)
104
+ }
105
+ })
106
+
107
+ it('should seed correct number of categories', async () => {
108
+ await seedPluginsIfEmpty()
109
+
110
+ const client = await getRawClient()
111
+ if (client && 'execute' in client) {
112
+ const result = await client.execute('SELECT COUNT(*) as count FROM plugin_categories')
113
+ const count = (result.rows[0] as unknown as { count: number })?.count ?? 0
114
+ expect(count).toBe(5)
115
+ }
116
+ })
117
+
118
+ it('should include both approved and pending plugins', async () => {
119
+ await seedPluginsIfEmpty()
120
+
121
+ const client = await getRawClient()
122
+ if (client && 'execute' in client) {
123
+ const approved = await client.execute(
124
+ "SELECT COUNT(*) as count FROM plugins WHERE status = 'approved'"
125
+ )
126
+ const pending = await client.execute(
127
+ "SELECT COUNT(*) as count FROM plugins WHERE status = 'pending'"
128
+ )
129
+
130
+ const approvedCount = (approved.rows[0] as unknown as { count: number })?.count ?? 0
131
+ const pendingCount = (pending.rows[0] as unknown as { count: number })?.count ?? 0
132
+
133
+ expect(approvedCount).toBeGreaterThan(0)
134
+ expect(pendingCount).toBeGreaterThan(0)
135
+ }
136
+ })
137
+
138
+ it('should include featured and non-featured plugins', async () => {
139
+ await seedPluginsIfEmpty()
140
+
141
+ const client = await getRawClient()
142
+ if (client && 'execute' in client) {
143
+ const featured = await client.execute(
144
+ 'SELECT COUNT(*) as count FROM plugins WHERE featured = 1'
145
+ )
146
+ const nonFeatured = await client.execute(
147
+ 'SELECT COUNT(*) as count FROM plugins WHERE featured = 0'
148
+ )
149
+
150
+ const featuredCount = (featured.rows[0] as unknown as { count: number })?.count ?? 0
151
+ const nonFeaturedCount = (nonFeatured.rows[0] as unknown as { count: number })?.count ?? 0
152
+
153
+ expect(featuredCount).toBeGreaterThan(0)
154
+ expect(nonFeaturedCount).toBeGreaterThan(0)
155
+ }
156
+ })
157
+
158
+ it('should be idempotent — second call does not add more data', async () => {
159
+ await seedPluginsIfEmpty()
160
+ await seedPluginsIfEmpty()
161
+
162
+ const client = await getRawClient()
163
+ if (client && 'execute' in client) {
164
+ const result = await client.execute('SELECT COUNT(*) as count FROM plugins')
165
+ const count = (result.rows[0] as unknown as { count: number })?.count ?? 0
166
+ expect(count).toBe(25)
167
+ }
168
+ })
169
+ })
170
+ })
@@ -46,7 +46,10 @@ const pluginManifest: ModuleManifest = {
46
46
  dbSchemas: {
47
47
  files: ['plugins'],
48
48
  hasSeed: true,
49
+ seed: { serviceFile: 'plugin-seed-service', functionName: 'seedPluginsIfEmpty' },
49
50
  },
51
+
52
+ cliModule: { dir: 'plugin', registerFunction: 'registerPluginCommands' },
50
53
  }
51
54
 
52
55
  export default pluginManifest
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
2
+ import { createTestClient } from '@server/test-utils/test-client'
3
+ import { setupTestDatabase, cleanupTestDatabase } from '@server/db/test-setup'
4
+
5
+ describe('Tenant Routes', () => {
6
+ const authHeaders = { Authorization: 'Bearer test-super-admin-1' }
7
+
8
+ beforeAll(async () => {
9
+ await setupTestDatabase()
10
+ })
11
+
12
+ afterAll(async () => {
13
+ await cleanupTestDatabase()
14
+ })
15
+
16
+ describe('GET /api/tenants', () => {
17
+ it('should return list of tenants', async () => {
18
+ const client = createTestClient(undefined, { headers: authHeaders })
19
+ const res = await client.api.tenants.$get({ query: {} })
20
+ expect(res.status).toBe(200)
21
+
22
+ const data = await res.json()
23
+ expect(data.success).toBe(true)
24
+ if (data.success) {
25
+ expect(Array.isArray(data.data.items)).toBe(true)
26
+ expect(data.data).toHaveProperty('total')
27
+ expect(data.data).toHaveProperty('page')
28
+ expect(data.data).toHaveProperty('pageSize')
29
+ }
30
+ })
31
+
32
+ it('should support pagination', async () => {
33
+ const client = createTestClient(undefined, { headers: authHeaders })
34
+ const res = await client.api.tenants.$get({
35
+ query: { page: 1, pageSize: 5 },
36
+ })
37
+ expect(res.status).toBe(200)
38
+
39
+ const data = await res.json()
40
+ if (data.success) {
41
+ expect(data.data.page).toBe(1)
42
+ expect(data.data.pageSize).toBe(5)
43
+ }
44
+ })
45
+
46
+ it('should filter by status', async () => {
47
+ const client = createTestClient(undefined, { headers: authHeaders })
48
+ const res = await client.api.tenants.$get({
49
+ query: { status: 'active' },
50
+ })
51
+ expect(res.status).toBe(200)
52
+
53
+ const data = await res.json()
54
+ if (data.success) {
55
+ for (const item of data.data.items) {
56
+ expect(item.status).toBe('active')
57
+ }
58
+ }
59
+ })
60
+ })
61
+
62
+ describe('GET /api/tenants/:id', () => {
63
+ it('should return 404 for non-existent tenant', async () => {
64
+ const client = createTestClient(undefined, { headers: authHeaders })
65
+ const res = await client.api.tenants[':id'].$get({
66
+ param: { id: '99999' },
67
+ })
68
+ expect(res.status).toBe(404)
69
+ })
70
+
71
+ it('should return tenant by id', async () => {
72
+ const client = createTestClient(undefined, { headers: authHeaders })
73
+ // First list to get an id
74
+ const listRes = await client.api.tenants.$get({ query: {} })
75
+ const listData = await listRes.json()
76
+
77
+ if (listData.success && listData.data.items.length > 0) {
78
+ const tenantId = String(listData.data.items[0].id)
79
+ const res = await client.api.tenants[':id'].$get({
80
+ param: { id: tenantId },
81
+ })
82
+ expect(res.status).toBe(200)
83
+
84
+ const data = await res.json()
85
+ if (data.success) {
86
+ expect(data.data).toHaveProperty('id')
87
+ expect(data.data).toHaveProperty('name')
88
+ expect(data.data).toHaveProperty('slug')
89
+ expect(data.data).toHaveProperty('status')
90
+ expect(data.data).toHaveProperty('plan')
91
+ }
92
+ }
93
+ })
94
+ })
95
+
96
+ describe('POST /api/tenants', () => {
97
+ it('should create a new tenant', async () => {
98
+ const client = createTestClient(undefined, { headers: authHeaders })
99
+ const res = await client.api.tenants.$post({
100
+ json: {
101
+ name: 'Test Tenant Route',
102
+ slug: 'test-tenant-route',
103
+ plan: 'free',
104
+ maxUsers: 10,
105
+ settings: null,
106
+ },
107
+ })
108
+ expect(res.status).toBe(201)
109
+
110
+ const data = await res.json()
111
+ if (data.success) {
112
+ expect(data.data.name).toBe('Test Tenant Route')
113
+ expect(data.data.slug).toBe('test-tenant-route')
114
+ expect(data.data.status).toBe('trial')
115
+ }
116
+ })
117
+
118
+ it('should reject duplicate slug', async () => {
119
+ const client = createTestClient(undefined, { headers: authHeaders })
120
+ const res = await client.api.tenants.$post({
121
+ json: {
122
+ name: 'Duplicate',
123
+ slug: 'demo', // seeded slug
124
+ plan: 'free',
125
+ maxUsers: 5,
126
+ settings: null,
127
+ },
128
+ })
129
+ expect(res.status).toBe(400)
130
+ })
131
+ })
132
+
133
+ describe('DELETE /api/tenants/:id', () => {
134
+ it('should return 404 for non-existent tenant', async () => {
135
+ const client = createTestClient(undefined, { headers: authHeaders })
136
+ const res = await client.api.tenants[':id'].$delete({
137
+ param: { id: '99999' },
138
+ })
139
+ expect(res.status).toBe(404)
140
+ })
141
+ })
142
+ })