create-fullstack-scaffold 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +499 -100
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -2
- package/template/CLAUDE.md +18 -6
- package/template/drizzle/0004_add_merchants_products.sql +30 -0
- package/template/drizzle/meta/_journal.json +7 -0
- package/template/eslint-rules/module-boundary.js +6 -2
- package/template/eslint-rules/no-cross-module-service-import.js +105 -0
- package/template/eslint-rules/no-disable-type-safe-client.js +5 -0
- package/template/eslint.config.js +9 -0
- package/template/lint-scripts/config/project.config.ts +21 -0
- package/template/lint-scripts/validate-all.ts +48 -12
- package/template/lint-scripts/validators/index.ts +29 -0
- package/template/lint-scripts/validators/module-public-api.validator.ts +103 -0
- package/template/lint-scripts/validators/schema-uniqueness.validator.ts +147 -0
- package/template/modules.config.ts +2 -0
- package/template/package.json +2 -0
- package/template/playwright.config.ts +14 -0
- package/template/src/admin/App.tsx +40 -3
- package/template/src/admin/components/LanguageSwitcher.tsx +22 -0
- package/template/src/admin/components/NotificationDrawer.tsx +108 -38
- package/template/src/admin/components/StatsCard.tsx +3 -1
- package/template/src/admin/components/ThemeToggle.tsx +28 -0
- package/template/src/admin/hooks/useRoles.ts +2 -2
- package/template/src/admin/i18n/index.ts +18 -0
- package/template/src/admin/i18n/locales/en-US.json +381 -0
- package/template/src/admin/i18n/locales/zh-CN.json +381 -0
- package/template/src/admin/i18n/useLanguage.ts +21 -0
- package/template/src/admin/layouts/Header.tsx +37 -16
- package/template/src/admin/layouts/Layout.tsx +7 -3
- package/template/src/admin/layouts/Sidebar.tsx +70 -68
- package/template/src/admin/main.tsx +1 -0
- package/template/src/admin/pages/ContentPage.tsx +68 -56
- package/template/src/admin/pages/DashboardPage.tsx +82 -76
- package/template/src/admin/pages/DisputesPage.tsx +61 -53
- package/template/src/admin/pages/LoginPage.tsx +41 -33
- package/template/src/admin/pages/OrdersPage.tsx +69 -64
- package/template/src/admin/pages/PermissionsPage.tsx +10 -8
- package/template/src/admin/pages/PluginDashboardPage.tsx +3 -1
- package/template/src/admin/pages/PluginManagementPage.tsx +3 -1
- package/template/src/admin/pages/RegisterPage.tsx +18 -16
- package/template/src/admin/pages/RolesPage.tsx +51 -49
- package/template/src/admin/pages/SettingsPage.tsx +22 -22
- package/template/src/admin/pages/SystemLogsPage.tsx +32 -30
- package/template/src/admin/pages/TicketsPage.tsx +67 -59
- package/template/src/admin/pages/UsersPage.tsx +63 -53
- package/template/src/admin/pages/__tests__/RolesPage.test.tsx +2 -2
- package/template/src/admin/stores/themeStore.ts +32 -0
- package/template/src/client/index.css +73 -0
- package/template/src/merchant/App.tsx +2 -0
- package/template/src/merchant/components/MerchantGuard.tsx +2 -6
- package/template/src/merchant/components/__tests__/MerchantGuard.test.tsx +117 -0
- package/template/src/merchant/layouts/Header.tsx +2 -2
- package/template/src/merchant/pages/DashboardPage.tsx +2 -2
- package/template/src/merchant/pages/DisputesPage.tsx +14 -10
- package/template/src/merchant/pages/LoginPage.tsx +49 -0
- package/template/src/merchant/pages/OrdersPage.tsx +17 -10
- package/template/src/merchant/pages/ProductsPage.tsx +31 -8
- package/template/src/merchant/pages/SettingsPage.tsx +3 -7
- package/template/src/merchant/stores/__tests__/merchantStore.test.ts +317 -0
- package/template/src/merchant/stores/merchantStore.ts +24 -27
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/merchants.ts +26 -0
- package/template/src/server/db/schema/products.ts +25 -0
- package/template/src/server/db/test-setup.ts +237 -0
- package/template/src/server/middleware/__tests__/audit-log.test.ts +144 -0
- package/template/src/server/middleware/__tests__/cors.test.ts +84 -0
- package/template/src/server/middleware/__tests__/logger.test.ts +117 -0
- package/template/src/server/middleware/__tests__/rate-limit.test.ts +73 -0
- package/template/src/server/middleware/__tests__/realtime-env.test.ts +56 -0
- package/template/src/server/middleware/__tests__/tenant-isolation.test.ts +150 -0
- package/template/src/server/module-admin/__tests__/admin-routes.test.ts +1 -1
- package/template/src/server/module-admin/module.ts +4 -0
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +3 -3
- package/template/src/server/module-admin/routes/user-management-routes.ts +2 -2
- package/template/src/server/module-auth/__tests__/auth-routes.test.ts +315 -0
- package/template/src/server/module-auth/__tests__/profile-routes.test.ts +83 -0
- package/template/src/server/module-auth/module.ts +2 -0
- package/template/src/server/module-content/module.ts +1 -0
- package/template/src/server/module-content/routes/content-routes.ts +2 -2
- package/template/src/server/module-dispute/module.ts +1 -0
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -2
- package/template/src/server/module-merchant/__tests__/merchant-routes.test.ts +218 -0
- package/template/src/server/module-merchant/__tests__/merchant-service.test.ts +183 -0
- package/template/src/server/module-merchant/index.ts +10 -0
- package/template/src/server/module-merchant/module.ts +33 -0
- package/template/src/server/module-merchant/routes/merchant-routes.ts +138 -0
- package/template/src/server/module-merchant/services/merchant-service.ts +292 -0
- package/template/src/server/module-notifications/index.ts +18 -0
- package/template/src/server/module-notifications/module.ts +2 -0
- package/template/src/server/module-order/module.ts +1 -0
- package/template/src/server/module-order/routes/order-routes.ts +2 -2
- package/template/src/server/module-permission/routes/role-routes.ts +3 -3
- package/template/src/server/module-plugin/__tests__/admin-category-service.test.ts +181 -0
- package/template/src/server/module-plugin/__tests__/admin-plugin-service.test.ts +277 -0
- package/template/src/server/module-plugin/__tests__/admin-stats-service.test.ts +170 -0
- package/template/src/server/module-plugin/__tests__/plugin-review-service.test.ts +260 -0
- package/template/src/server/module-plugin/__tests__/plugin-seed-service.test.ts +170 -0
- package/template/src/server/module-plugin/module.ts +3 -0
- package/template/src/server/module-tenant/__tests__/tenant-routes.test.ts +142 -0
- package/template/src/server/module-tenant/__tests__/tenant-service.test.ts +152 -0
- package/template/src/server/module-tenant/module.ts +1 -0
- package/template/src/server/module-tenant/services/tenant-service.ts +1 -37
- package/template/src/server/module-ticket/module.ts +1 -0
- package/template/src/server/module-ticket/routes/ticket-routes.ts +2 -2
- package/template/src/server/module-todos/module.ts +3 -0
- package/template/src/server/route-registry.ts +4 -0
- package/template/src/server/utils/__tests__/date.test.ts +130 -0
- package/template/src/server/utils/__tests__/env.test.ts +25 -0
- package/template/src/server/utils/__tests__/generate.test.ts +82 -0
- package/template/src/server/utils/__tests__/id-helpers.test.ts +29 -0
- package/template/src/server/utils/__tests__/json.test.ts +49 -0
- package/template/src/server/utils/__tests__/permission-utils.test.ts +26 -0
- package/template/src/server/utils/__tests__/uuid.test.ts +25 -0
- package/template/src/shared/core/module-manifest.ts +15 -0
- package/template/src/shared/modules/admin/schemas.ts +1 -1
- package/template/src/shared/modules/content/schemas.ts +2 -2
- package/template/src/shared/modules/dispute/schemas.ts +2 -2
- package/template/src/shared/modules/index.ts +181 -1
- package/template/src/shared/modules/merchant/index.ts +12 -0
- package/template/src/shared/modules/merchant/schemas.ts +63 -0
- package/template/src/shared/modules/order/schemas.ts +2 -2
- package/template/src/shared/modules/permission/index.ts +1 -1
- package/template/src/shared/modules/permission/schemas.ts +1 -1
- package/template/src/shared/modules/role/schemas.ts +2 -2
- package/template/src/shared/modules/ticket/schemas.ts +2 -2
- package/template/src/shared/schemas/index.ts +74 -2
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import * as adminService from '../services/admin-plugin-service'
|
|
3
|
+
import { getRawClient, getDb } from '@server/db'
|
|
4
|
+
import { setupTestDatabase, cleanupTestDatabase } from '@server/db/test-setup'
|
|
5
|
+
import { NotFoundError } 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 ?? `p-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`) as string
|
|
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,
|
|
28
|
+
(overrides.name ?? 'Test Plugin') as string,
|
|
29
|
+
(overrides.slug ?? id) 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 ?? 'pending') 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
|
+
describe('Admin Plugin Service', () => {
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
await setupTestDatabase()
|
|
48
|
+
const db = await getDb()
|
|
49
|
+
expect(db).toBeDefined()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await cleanupTestDatabase()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
await clearPluginTables()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await clearPluginTables()
|
|
62
|
+
vi.clearAllMocks()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('listPending', () => {
|
|
66
|
+
it('should return pending plugins with pagination', async () => {
|
|
67
|
+
await insertTestPlugin({ slug: 'pending-1', status: 'pending' })
|
|
68
|
+
await insertTestPlugin({ slug: 'pending-2', status: 'pending' })
|
|
69
|
+
await insertTestPlugin({ slug: 'approved-1', status: 'approved' })
|
|
70
|
+
|
|
71
|
+
const result = await adminService.listPending({ page: 1, limit: 10 })
|
|
72
|
+
|
|
73
|
+
expect(result.plugins).toHaveLength(2)
|
|
74
|
+
expect(result.plugins.every(p => p.status === 'pending')).toBe(true)
|
|
75
|
+
expect(result.page).toBe(1)
|
|
76
|
+
expect(result.limit).toBe(10)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should return empty when no pending plugins', async () => {
|
|
80
|
+
await insertTestPlugin({ slug: 'no-pending', status: 'approved' })
|
|
81
|
+
|
|
82
|
+
const result = await adminService.listPending()
|
|
83
|
+
|
|
84
|
+
expect(result.plugins).toEqual([])
|
|
85
|
+
expect(result.total).toBe(0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should paginate correctly', async () => {
|
|
89
|
+
for (let i = 0; i < 5; i++) {
|
|
90
|
+
await insertTestPlugin({ slug: `pend-page-${i}`, status: 'pending' })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const page1 = await adminService.listPending({ page: 1, limit: 2 })
|
|
94
|
+
const page2 = await adminService.listPending({ page: 2, limit: 2 })
|
|
95
|
+
|
|
96
|
+
expect(page1.plugins).toHaveLength(2)
|
|
97
|
+
expect(page1.total).toBe(5)
|
|
98
|
+
expect(page2.plugins).toHaveLength(2)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('approvePlugin', () => {
|
|
103
|
+
it('should approve a pending plugin', async () => {
|
|
104
|
+
await insertTestPlugin({ slug: 'approve-me', status: 'pending' })
|
|
105
|
+
|
|
106
|
+
const result = await adminService.approvePlugin('approve-me')
|
|
107
|
+
|
|
108
|
+
expect(result.status).toBe('approved')
|
|
109
|
+
expect(result.slug).toBe('approve-me')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should clear reject reason on approval', async () => {
|
|
113
|
+
const client = await getRawClient()
|
|
114
|
+
await insertTestPlugin({ slug: 're-approve' })
|
|
115
|
+
if (client && 'execute' in client) {
|
|
116
|
+
await client.execute("UPDATE plugins SET reject_reason = 'bad' WHERE slug = 're-approve'")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await adminService.approvePlugin('re-approve')
|
|
120
|
+
|
|
121
|
+
expect(result.status).toBe('approved')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should throw NotFoundError for non-existent slug', async () => {
|
|
125
|
+
await expect(adminService.approvePlugin('non-existent')).rejects.toThrow(NotFoundError)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('rejectPlugin', () => {
|
|
130
|
+
it('should reject a plugin with reason', async () => {
|
|
131
|
+
await insertTestPlugin({ slug: 'reject-me', status: 'pending' })
|
|
132
|
+
|
|
133
|
+
const result = await adminService.rejectPlugin('reject-me', 'Spam content')
|
|
134
|
+
|
|
135
|
+
expect(result.status).toBe('rejected')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should throw NotFoundError for non-existent slug', async () => {
|
|
139
|
+
await expect(adminService.rejectPlugin('non-existent', 'reason')).rejects.toThrow(
|
|
140
|
+
NotFoundError
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('toggleFeatured', () => {
|
|
146
|
+
it('should toggle featured from false to true', async () => {
|
|
147
|
+
await insertTestPlugin({ slug: 'feature-me', featured: false })
|
|
148
|
+
|
|
149
|
+
const result = await adminService.toggleFeatured('feature-me')
|
|
150
|
+
|
|
151
|
+
expect(result.featured).toBe(true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should toggle featured from true to false', async () => {
|
|
155
|
+
await insertTestPlugin({ slug: 'unfeature-me', featured: true })
|
|
156
|
+
|
|
157
|
+
const result = await adminService.toggleFeatured('unfeature-me')
|
|
158
|
+
|
|
159
|
+
expect(result.featured).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should throw NotFoundError for non-existent slug', async () => {
|
|
163
|
+
await expect(adminService.toggleFeatured('non-existent')).rejects.toThrow(NotFoundError)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('adminRemovePlugin', () => {
|
|
168
|
+
it('should remove a plugin', async () => {
|
|
169
|
+
await insertTestPlugin({ slug: 'remove-me' })
|
|
170
|
+
|
|
171
|
+
await adminService.adminRemovePlugin('remove-me')
|
|
172
|
+
|
|
173
|
+
const client = await getRawClient()
|
|
174
|
+
if (client && 'execute' in client) {
|
|
175
|
+
const result = await client.execute('SELECT * FROM plugins WHERE slug = ?', ['remove-me'])
|
|
176
|
+
expect(result.rows.length).toBe(0)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should throw NotFoundError for non-existent slug', async () => {
|
|
181
|
+
await expect(adminService.adminRemovePlugin('non-existent')).rejects.toThrow(NotFoundError)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('listAllPlugins', () => {
|
|
186
|
+
it('should list all plugins without filter', async () => {
|
|
187
|
+
await insertTestPlugin({ slug: 'all-1', status: 'approved' })
|
|
188
|
+
await insertTestPlugin({ slug: 'all-2', status: 'pending' })
|
|
189
|
+
await insertTestPlugin({ slug: 'all-3', status: 'rejected' })
|
|
190
|
+
|
|
191
|
+
const result = await adminService.listAllPlugins()
|
|
192
|
+
|
|
193
|
+
expect(result.plugins).toHaveLength(3)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should filter by status', async () => {
|
|
197
|
+
await insertTestPlugin({ slug: 'filter-approved', status: 'approved' })
|
|
198
|
+
await insertTestPlugin({ slug: 'filter-pending', status: 'pending' })
|
|
199
|
+
|
|
200
|
+
const result = await adminService.listAllPlugins({ status: 'approved' })
|
|
201
|
+
|
|
202
|
+
expect(result.plugins).toHaveLength(1)
|
|
203
|
+
expect(result.plugins[0].status).toBe('approved')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should paginate correctly', async () => {
|
|
207
|
+
for (let i = 0; i < 5; i++) {
|
|
208
|
+
await insertTestPlugin({ slug: `page-all-${i}`, status: 'approved' })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const result = await adminService.listAllPlugins({ page: 1, limit: 2 })
|
|
212
|
+
|
|
213
|
+
expect(result.plugins).toHaveLength(2)
|
|
214
|
+
expect(result.total).toBe(5)
|
|
215
|
+
expect(result.page).toBe(1)
|
|
216
|
+
expect(result.limit).toBe(2)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should return empty when no plugins', async () => {
|
|
220
|
+
const result = await adminService.listAllPlugins()
|
|
221
|
+
|
|
222
|
+
expect(result.plugins).toEqual([])
|
|
223
|
+
expect(result.total).toBe(0)
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('bulkApprove', () => {
|
|
228
|
+
it('should approve multiple plugins', async () => {
|
|
229
|
+
await insertTestPlugin({ slug: 'bulk-a1', status: 'pending' })
|
|
230
|
+
await insertTestPlugin({ slug: 'bulk-a2', status: 'pending' })
|
|
231
|
+
await insertTestPlugin({ slug: 'bulk-a3', status: 'pending' })
|
|
232
|
+
|
|
233
|
+
const count = await adminService.bulkApprove(['bulk-a1', 'bulk-a2', 'bulk-a3'])
|
|
234
|
+
|
|
235
|
+
expect(count).toBe(3)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should skip non-existent slugs', async () => {
|
|
239
|
+
await insertTestPlugin({ slug: 'bulk-exist', status: 'pending' })
|
|
240
|
+
|
|
241
|
+
const count = await adminService.bulkApprove(['bulk-exist', 'bulk-nope'])
|
|
242
|
+
|
|
243
|
+
expect(count).toBe(1)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should return 0 for empty array', async () => {
|
|
247
|
+
const count = await adminService.bulkApprove([])
|
|
248
|
+
|
|
249
|
+
expect(count).toBe(0)
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('bulkReject', () => {
|
|
254
|
+
it('should reject multiple plugins', async () => {
|
|
255
|
+
await insertTestPlugin({ slug: 'bulk-r1', status: 'pending' })
|
|
256
|
+
await insertTestPlugin({ slug: 'bulk-r2', status: 'pending' })
|
|
257
|
+
|
|
258
|
+
const count = await adminService.bulkReject(['bulk-r1', 'bulk-r2'], 'Not suitable')
|
|
259
|
+
|
|
260
|
+
expect(count).toBe(2)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should skip non-existent slugs', async () => {
|
|
264
|
+
await insertTestPlugin({ slug: 'bulk-rexist', status: 'pending' })
|
|
265
|
+
|
|
266
|
+
const count = await adminService.bulkReject(['bulk-rexist', 'bulk-rnope'], 'Reason')
|
|
267
|
+
|
|
268
|
+
expect(count).toBe(1)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('should return 0 for empty array', async () => {
|
|
272
|
+
const count = await adminService.bulkReject([], 'Reason')
|
|
273
|
+
|
|
274
|
+
expect(count).toBe(0)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
})
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import * as statsService from '../services/admin-stats-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_reviews')
|
|
10
|
+
await client.execute('DELETE FROM plugin_versions')
|
|
11
|
+
await client.execute('DELETE FROM plugin_category_mappings')
|
|
12
|
+
await client.execute('DELETE FROM plugins')
|
|
13
|
+
await client.execute('DELETE FROM plugin_categories')
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function insertPlugin(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 ?? `p-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`) as string
|
|
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,
|
|
28
|
+
(overrides.name ?? 'Test Plugin') as string,
|
|
29
|
+
(overrides.slug ?? id) 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 insertCategory(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 ?? `cat-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`) as string
|
|
50
|
+
await client.execute({
|
|
51
|
+
sql: `INSERT INTO plugin_categories (id, name, slug, description, icon, sort_order)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
53
|
+
args: [
|
|
54
|
+
id,
|
|
55
|
+
(overrides.name ?? 'Test Category') as string,
|
|
56
|
+
(overrides.slug ?? id) as string,
|
|
57
|
+
(overrides.description ?? null) as string | null,
|
|
58
|
+
(overrides.icon ?? null) as string | null,
|
|
59
|
+
(overrides.sortOrder ?? 0) as number,
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
return id
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('Admin Stats Service', () => {
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
await setupTestDatabase()
|
|
68
|
+
const db = await getDb()
|
|
69
|
+
expect(db).toBeDefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
afterAll(async () => {
|
|
73
|
+
await cleanupTestDatabase()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
await clearPluginTables()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
await clearPluginTables()
|
|
82
|
+
vi.clearAllMocks()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('getDashboardStats', () => {
|
|
86
|
+
it('should return zeroed stats when no data exists', async () => {
|
|
87
|
+
const stats = await statsService.getDashboardStats()
|
|
88
|
+
|
|
89
|
+
expect(stats.totalPlugins).toBe(0)
|
|
90
|
+
expect(stats.pendingPlugins).toBe(0)
|
|
91
|
+
expect(stats.totalDownloads).toBe(0)
|
|
92
|
+
expect(stats.totalDevelopers).toBe(0)
|
|
93
|
+
expect(stats.totalCategories).toBe(0)
|
|
94
|
+
expect(stats.recentSubmissions).toEqual([])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should count total plugins', async () => {
|
|
98
|
+
await insertPlugin({ slug: 'a', status: 'approved' })
|
|
99
|
+
await insertPlugin({ slug: 'b', status: 'pending' })
|
|
100
|
+
await insertPlugin({ slug: 'c', status: 'rejected' })
|
|
101
|
+
|
|
102
|
+
const stats = await statsService.getDashboardStats()
|
|
103
|
+
|
|
104
|
+
expect(stats.totalPlugins).toBe(3)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should count pending plugins', async () => {
|
|
108
|
+
await insertPlugin({ slug: 'p1', status: 'pending' })
|
|
109
|
+
await insertPlugin({ slug: 'p2', status: 'pending' })
|
|
110
|
+
await insertPlugin({ slug: 'p3', status: 'approved' })
|
|
111
|
+
|
|
112
|
+
const stats = await statsService.getDashboardStats()
|
|
113
|
+
|
|
114
|
+
expect(stats.pendingPlugins).toBe(2)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should sum download counts', async () => {
|
|
118
|
+
await insertPlugin({ slug: 'd1', downloadCount: 100, status: 'approved' })
|
|
119
|
+
await insertPlugin({ slug: 'd2', downloadCount: 50, status: 'approved' })
|
|
120
|
+
|
|
121
|
+
const stats = await statsService.getDashboardStats()
|
|
122
|
+
|
|
123
|
+
expect(stats.totalDownloads).toBe(150)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should count distinct developers', async () => {
|
|
127
|
+
await insertPlugin({ slug: 'dev1', authorId: 'user-1', status: 'approved' })
|
|
128
|
+
await insertPlugin({ slug: 'dev2', authorId: 'user-1', status: 'approved' })
|
|
129
|
+
await insertPlugin({ slug: 'dev3', authorId: 'user-2', status: 'approved' })
|
|
130
|
+
|
|
131
|
+
const stats = await statsService.getDashboardStats()
|
|
132
|
+
|
|
133
|
+
expect(stats.totalDevelopers).toBe(2)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should count categories', async () => {
|
|
137
|
+
await insertCategory({ name: 'UI', slug: 'ui' })
|
|
138
|
+
await insertCategory({ name: 'SEO', slug: 'seo' })
|
|
139
|
+
|
|
140
|
+
const stats = await statsService.getDashboardStats()
|
|
141
|
+
|
|
142
|
+
expect(stats.totalCategories).toBe(2)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should return recent submissions (up to 5)', async () => {
|
|
146
|
+
for (let i = 0; i < 7; i++) {
|
|
147
|
+
await insertPlugin({ slug: `recent-${i}`, status: 'approved' })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const stats = await statsService.getDashboardStats()
|
|
151
|
+
|
|
152
|
+
expect(stats.recentSubmissions!.length).toBeLessThanOrEqual(5)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should return full stats with mixed data', async () => {
|
|
156
|
+
await insertPlugin({ slug: 'full-1', status: 'approved', downloadCount: 200, authorId: 'dev-a' })
|
|
157
|
+
await insertPlugin({ slug: 'full-2', status: 'pending', downloadCount: 10, authorId: 'dev-b' })
|
|
158
|
+
await insertCategory({ name: 'Full Cat', slug: 'full-cat' })
|
|
159
|
+
|
|
160
|
+
const stats = await statsService.getDashboardStats()
|
|
161
|
+
|
|
162
|
+
expect(stats.totalPlugins).toBe(2)
|
|
163
|
+
expect(stats.pendingPlugins).toBe(1)
|
|
164
|
+
expect(stats.totalDownloads).toBe(210)
|
|
165
|
+
expect(stats.totalDevelopers).toBe(2)
|
|
166
|
+
expect(stats.totalCategories).toBe(1)
|
|
167
|
+
expect(stats.recentSubmissions).toHaveLength(2)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
})
|