create-blitzpack 0.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.
- package/dist/index.js +452 -0
- package/package.json +57 -0
- package/template/.dockerignore +59 -0
- package/template/.github/workflows/ci.yml +157 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.husky/pre-push +1 -0
- package/template/.lintstagedrc.cjs +4 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierrc +9 -0
- package/template/.vscode/settings.json +13 -0
- package/template/CLAUDE.md +175 -0
- package/template/CONTRIBUTING.md +32 -0
- package/template/Dockerfile +90 -0
- package/template/GETTING_STARTED.md +35 -0
- package/template/LICENSE +21 -0
- package/template/README.md +116 -0
- package/template/apps/api/.dockerignore +51 -0
- package/template/apps/api/.env.local.example +62 -0
- package/template/apps/api/emails/account-deleted-email.tsx +69 -0
- package/template/apps/api/emails/components/email-layout.tsx +154 -0
- package/template/apps/api/emails/config.ts +22 -0
- package/template/apps/api/emails/password-changed-email.tsx +88 -0
- package/template/apps/api/emails/password-reset-email.tsx +86 -0
- package/template/apps/api/emails/verification-email.tsx +85 -0
- package/template/apps/api/emails/welcome-email.tsx +70 -0
- package/template/apps/api/package.json +84 -0
- package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
- package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
- package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
- package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
- package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
- package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
- package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
- package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +116 -0
- package/template/apps/api/prisma/seed.ts +159 -0
- package/template/apps/api/prisma.config.ts +14 -0
- package/template/apps/api/src/app.ts +377 -0
- package/template/apps/api/src/common/logger.service.ts +227 -0
- package/template/apps/api/src/config/env.ts +60 -0
- package/template/apps/api/src/config/rate-limit.ts +29 -0
- package/template/apps/api/src/hooks/auth.ts +122 -0
- package/template/apps/api/src/plugins/auth.ts +198 -0
- package/template/apps/api/src/plugins/database.ts +45 -0
- package/template/apps/api/src/plugins/logger.ts +33 -0
- package/template/apps/api/src/plugins/multipart.ts +16 -0
- package/template/apps/api/src/plugins/scalar.ts +20 -0
- package/template/apps/api/src/plugins/schedule.ts +52 -0
- package/template/apps/api/src/plugins/services.ts +66 -0
- package/template/apps/api/src/plugins/swagger.ts +56 -0
- package/template/apps/api/src/routes/accounts.ts +91 -0
- package/template/apps/api/src/routes/admin-sessions.ts +92 -0
- package/template/apps/api/src/routes/metrics.ts +71 -0
- package/template/apps/api/src/routes/password.ts +46 -0
- package/template/apps/api/src/routes/sessions.ts +53 -0
- package/template/apps/api/src/routes/stats.ts +38 -0
- package/template/apps/api/src/routes/uploads-serve.ts +27 -0
- package/template/apps/api/src/routes/uploads.ts +154 -0
- package/template/apps/api/src/routes/users.ts +114 -0
- package/template/apps/api/src/routes/verification.ts +90 -0
- package/template/apps/api/src/server.ts +34 -0
- package/template/apps/api/src/services/accounts.service.ts +125 -0
- package/template/apps/api/src/services/authorization.service.ts +162 -0
- package/template/apps/api/src/services/email.service.ts +170 -0
- package/template/apps/api/src/services/file-storage.service.ts +267 -0
- package/template/apps/api/src/services/metrics.service.ts +175 -0
- package/template/apps/api/src/services/password.service.ts +56 -0
- package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
- package/template/apps/api/src/services/sessions.service.ts +276 -0
- package/template/apps/api/src/services/stats.service.ts +273 -0
- package/template/apps/api/src/services/uploads.service.ts +163 -0
- package/template/apps/api/src/services/users.service.spec.ts +249 -0
- package/template/apps/api/src/services/users.service.ts +198 -0
- package/template/apps/api/src/utils/file-validation.ts +108 -0
- package/template/apps/api/start.sh +33 -0
- package/template/apps/api/test/helpers/fastify-app.ts +24 -0
- package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
- package/template/apps/api/test/helpers/mock-logger.ts +28 -0
- package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
- package/template/apps/api/test/helpers/test-db.ts +125 -0
- package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
- package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
- package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
- package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
- package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
- package/template/apps/api/test/setup.ts +31 -0
- package/template/apps/api/tsconfig.json +26 -0
- package/template/apps/api/vitest.config.ts +35 -0
- package/template/apps/web/.env.local.example +11 -0
- package/template/apps/web/components.json +24 -0
- package/template/apps/web/next.config.ts +22 -0
- package/template/apps/web/package.json +56 -0
- package/template/apps/web/postcss.config.js +5 -0
- package/template/apps/web/public/apple-icon.png +0 -0
- package/template/apps/web/public/icon.png +0 -0
- package/template/apps/web/public/robots.txt +3 -0
- package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
- package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
- package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
- package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
- package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
- package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
- package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
- package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
- package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
- package/template/apps/web/src/app/dashboard/page.tsx +296 -0
- package/template/apps/web/src/app/error.tsx +32 -0
- package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/global-error.tsx +96 -0
- package/template/apps/web/src/app/globals.css +22 -0
- package/template/apps/web/src/app/icon.png +0 -0
- package/template/apps/web/src/app/layout.tsx +34 -0
- package/template/apps/web/src/app/not-found.tsx +28 -0
- package/template/apps/web/src/app/page.tsx +192 -0
- package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
- package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
- package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
- package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
- package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
- package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
- package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
- package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
- package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
- package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
- package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
- package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
- package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
- package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
- package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
- package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
- package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
- package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
- package/template/apps/web/src/components/auth/github-button.tsx +40 -0
- package/template/apps/web/src/components/auth/google-button.tsx +54 -0
- package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
- package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
- package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
- package/template/apps/web/src/components/error/error-card.tsx +47 -0
- package/template/apps/web/src/components/error/forbidden.tsx +25 -0
- package/template/apps/web/src/components/landing/command-block.tsx +64 -0
- package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
- package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
- package/template/apps/web/src/components/landing/logo.tsx +41 -0
- package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
- package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
- package/template/apps/web/src/components/layout/footer.tsx +3 -0
- package/template/apps/web/src/config/landing-data.ts +152 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
- package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
- package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
- package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
- package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
- package/template/apps/web/src/hooks/api/use-users.ts +149 -0
- package/template/apps/web/src/hooks/use-mobile.ts +21 -0
- package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
- package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
- package/template/apps/web/src/lib/api.ts +151 -0
- package/template/apps/web/src/lib/auth.ts +13 -0
- package/template/apps/web/src/lib/env.ts +52 -0
- package/template/apps/web/src/lib/form-utils.ts +11 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/providers.tsx +34 -0
- package/template/apps/web/src/store/atoms.ts +15 -0
- package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
- package/template/apps/web/src/test/setup.ts +8 -0
- package/template/apps/web/tailwind.config.ts +5 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/vitest.config.ts +32 -0
- package/template/assets/logo-512.png +0 -0
- package/template/assets/logo.svg +4 -0
- package/template/docker-compose.prod.yml +66 -0
- package/template/docker-compose.yml +36 -0
- package/template/eslint.config.ts +119 -0
- package/template/package.json +77 -0
- package/template/packages/tailwind-config/package.json +9 -0
- package/template/packages/tailwind-config/theme.css +179 -0
- package/template/packages/types/package.json +29 -0
- package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
- package/template/packages/types/src/api-response.ts +53 -0
- package/template/packages/types/src/health-check.ts +11 -0
- package/template/packages/types/src/pagination.ts +41 -0
- package/template/packages/types/src/role.ts +5 -0
- package/template/packages/types/src/session.ts +48 -0
- package/template/packages/types/src/stats.ts +113 -0
- package/template/packages/types/src/upload.ts +51 -0
- package/template/packages/types/src/user.ts +36 -0
- package/template/packages/types/tsconfig.json +5 -0
- package/template/packages/types/vitest.config.ts +21 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +108 -0
- package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
- package/template/packages/ui/src/alert-dialog.tsx +141 -0
- package/template/packages/ui/src/alert.tsx +66 -0
- package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
- package/template/packages/ui/src/avatar.tsx +53 -0
- package/template/packages/ui/src/badge.tsx +36 -0
- package/template/packages/ui/src/button.tsx +84 -0
- package/template/packages/ui/src/card.tsx +92 -0
- package/template/packages/ui/src/checkbox.tsx +32 -0
- package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
- package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
- package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
- package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
- package/template/packages/ui/src/data-table/data-table.tsx +167 -0
- package/template/packages/ui/src/dialog.tsx +143 -0
- package/template/packages/ui/src/dropdown-menu.tsx +257 -0
- package/template/packages/ui/src/empty-state.tsx +52 -0
- package/template/packages/ui/src/file-upload-input.tsx +202 -0
- package/template/packages/ui/src/form.tsx +168 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
- package/template/packages/ui/src/input.tsx +21 -0
- package/template/packages/ui/src/label.tsx +24 -0
- package/template/packages/ui/src/lib/utils.ts +6 -0
- package/template/packages/ui/src/password-input.tsx +102 -0
- package/template/packages/ui/src/popover.tsx +48 -0
- package/template/packages/ui/src/radio-group.tsx +45 -0
- package/template/packages/ui/src/scroll-area.tsx +58 -0
- package/template/packages/ui/src/select.tsx +187 -0
- package/template/packages/ui/src/separator.tsx +28 -0
- package/template/packages/ui/src/sheet.tsx +139 -0
- package/template/packages/ui/src/sidebar.tsx +726 -0
- package/template/packages/ui/src/skeleton-variants.tsx +87 -0
- package/template/packages/ui/src/skeleton.tsx +13 -0
- package/template/packages/ui/src/slider.tsx +63 -0
- package/template/packages/ui/src/sonner.tsx +25 -0
- package/template/packages/ui/src/spinner.tsx +16 -0
- package/template/packages/ui/src/switch.tsx +31 -0
- package/template/packages/ui/src/table.tsx +116 -0
- package/template/packages/ui/src/tabs.tsx +66 -0
- package/template/packages/ui/src/textarea.tsx +18 -0
- package/template/packages/ui/src/tooltip.tsx +61 -0
- package/template/packages/ui/src/user-avatar.tsx +97 -0
- package/template/packages/ui/test-config.js +3 -0
- package/template/packages/ui/tsconfig.json +12 -0
- package/template/packages/ui/turbo.json +18 -0
- package/template/packages/ui/vitest.config.ts +17 -0
- package/template/packages/ui/vitest.setup.ts +1 -0
- package/template/packages/utils/package.json +23 -0
- package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
- package/template/packages/utils/src/array.ts +18 -0
- package/template/packages/utils/src/async.ts +3 -0
- package/template/packages/utils/src/date.ts +77 -0
- package/template/packages/utils/src/errors.ts +73 -0
- package/template/packages/utils/src/number.ts +11 -0
- package/template/packages/utils/src/string.ts +13 -0
- package/template/packages/utils/tsconfig.json +5 -0
- package/template/packages/utils/vitest.config.ts +21 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.base.json +32 -0
- package/template/turbo.json +133 -0
- package/template/vitest.shared.ts +26 -0
- package/template/vitest.workspace.ts +9 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createMockAuthorizationService } from '@test/helpers/mock-authorization';
|
|
2
|
+
import { createMockLogger } from '@test/helpers/mock-logger';
|
|
3
|
+
import { createMockPrisma } from '@test/helpers/mock-prisma';
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import type { LoggerService } from '@/common/logger.service';
|
|
7
|
+
import type { PrismaClient } from '@/generated/client/client.js';
|
|
8
|
+
import type { AuthorizationService } from '@/services/authorization.service';
|
|
9
|
+
|
|
10
|
+
import { UsersService } from './users.service';
|
|
11
|
+
|
|
12
|
+
describe('UsersService', () => {
|
|
13
|
+
let service: UsersService;
|
|
14
|
+
let prisma: PrismaClient;
|
|
15
|
+
let logger: LoggerService;
|
|
16
|
+
let authorizationService: AuthorizationService;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
logger = createMockLogger();
|
|
20
|
+
prisma = createMockPrisma();
|
|
21
|
+
authorizationService = createMockAuthorizationService();
|
|
22
|
+
service = new UsersService(prisma, logger, authorizationService);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('getUsers', () => {
|
|
26
|
+
const defaultQuery = {
|
|
27
|
+
page: 1,
|
|
28
|
+
limit: 10,
|
|
29
|
+
sortBy: 'createdAt' as const,
|
|
30
|
+
sortOrder: 'desc' as const,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const mockUsers = [
|
|
34
|
+
{
|
|
35
|
+
id: '1',
|
|
36
|
+
email: 'user1@test.com',
|
|
37
|
+
name: 'User 1',
|
|
38
|
+
emailVerified: false,
|
|
39
|
+
image: null,
|
|
40
|
+
role: 'user' as const,
|
|
41
|
+
banned: false,
|
|
42
|
+
banReason: null,
|
|
43
|
+
banExpires: null,
|
|
44
|
+
createdAt: new Date(),
|
|
45
|
+
updatedAt: new Date(),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: '2',
|
|
49
|
+
email: 'user2@test.com',
|
|
50
|
+
name: 'User 2',
|
|
51
|
+
emailVerified: false,
|
|
52
|
+
image: null,
|
|
53
|
+
role: 'user' as const,
|
|
54
|
+
banned: false,
|
|
55
|
+
banReason: null,
|
|
56
|
+
banExpires: null,
|
|
57
|
+
createdAt: new Date(),
|
|
58
|
+
updatedAt: new Date(),
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
it('should return empty paginated response initially', async () => {
|
|
63
|
+
vi.mocked(prisma.user.findMany).mockResolvedValue([]);
|
|
64
|
+
vi.mocked(prisma.user.count).mockResolvedValue(0);
|
|
65
|
+
|
|
66
|
+
const result = await service.getUsers(defaultQuery);
|
|
67
|
+
expect(result.data).toEqual([]);
|
|
68
|
+
expect(result.pagination.total).toBe(0);
|
|
69
|
+
expect(result.pagination.totalPages).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return paginated users', async () => {
|
|
73
|
+
vi.mocked(prisma.user.findMany).mockResolvedValue(mockUsers);
|
|
74
|
+
vi.mocked(prisma.user.count).mockResolvedValue(2);
|
|
75
|
+
|
|
76
|
+
const result = await service.getUsers(defaultQuery);
|
|
77
|
+
expect(result.data).toHaveLength(2);
|
|
78
|
+
expect(result.pagination.total).toBe(2);
|
|
79
|
+
expect(result.pagination.totalPages).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle pagination correctly', async () => {
|
|
83
|
+
vi.mocked(prisma.user.findMany).mockResolvedValue([mockUsers[0]]);
|
|
84
|
+
vi.mocked(prisma.user.count).mockResolvedValue(3);
|
|
85
|
+
|
|
86
|
+
const result = await service.getUsers({
|
|
87
|
+
...defaultQuery,
|
|
88
|
+
page: 1,
|
|
89
|
+
limit: 2,
|
|
90
|
+
});
|
|
91
|
+
expect(result.data).toHaveLength(1);
|
|
92
|
+
expect(result.pagination.total).toBe(3);
|
|
93
|
+
expect(result.pagination.totalPages).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should filter users by search query', async () => {
|
|
97
|
+
vi.mocked(prisma.user.findMany).mockResolvedValue([mockUsers[0]]);
|
|
98
|
+
vi.mocked(prisma.user.count).mockResolvedValue(1);
|
|
99
|
+
|
|
100
|
+
const result = await service.getUsers({
|
|
101
|
+
...defaultQuery,
|
|
102
|
+
search: 'user1',
|
|
103
|
+
});
|
|
104
|
+
expect(result.data).toHaveLength(1);
|
|
105
|
+
expect(result.data[0].email).toBe('user1@test.com');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('getUserById', () => {
|
|
110
|
+
const mockUser = {
|
|
111
|
+
id: '1',
|
|
112
|
+
email: 'test@example.com',
|
|
113
|
+
name: 'Test User',
|
|
114
|
+
emailVerified: false,
|
|
115
|
+
image: null,
|
|
116
|
+
role: 'user' as const,
|
|
117
|
+
banned: false,
|
|
118
|
+
banReason: null,
|
|
119
|
+
banExpires: null,
|
|
120
|
+
createdAt: new Date(),
|
|
121
|
+
updatedAt: new Date(),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
it('should return user by ID', async () => {
|
|
125
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
|
126
|
+
|
|
127
|
+
const user = await service.getUserById('1');
|
|
128
|
+
|
|
129
|
+
expect(user).toEqual(mockUser);
|
|
130
|
+
expect(user?.id).toBe('1');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should throw NotFoundError when user not found', async () => {
|
|
134
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
|
135
|
+
|
|
136
|
+
await expect(service.getUserById('non-existent-id')).rejects.toThrow(
|
|
137
|
+
'User not found'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('createUser', () => {
|
|
143
|
+
const mockUser = {
|
|
144
|
+
id: '1',
|
|
145
|
+
email: 'test@example.com',
|
|
146
|
+
name: 'Test User',
|
|
147
|
+
emailVerified: false,
|
|
148
|
+
image: null,
|
|
149
|
+
role: 'user' as const,
|
|
150
|
+
banned: false,
|
|
151
|
+
banReason: null,
|
|
152
|
+
banExpires: null,
|
|
153
|
+
createdAt: new Date(),
|
|
154
|
+
updatedAt: new Date(),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
it('should create a new user', async () => {
|
|
158
|
+
vi.mocked(prisma.user.create).mockResolvedValue(mockUser);
|
|
159
|
+
|
|
160
|
+
const user = await service.createUser({
|
|
161
|
+
email: 'test@example.com',
|
|
162
|
+
name: 'Test User',
|
|
163
|
+
role: 'user',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(user.id).toBe('1');
|
|
167
|
+
expect(user.email).toBe('test@example.com');
|
|
168
|
+
expect(user.name).toBe('Test User');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('updateUser', () => {
|
|
173
|
+
const mockUser = {
|
|
174
|
+
id: '1',
|
|
175
|
+
email: 'test@example.com',
|
|
176
|
+
name: 'Test User',
|
|
177
|
+
emailVerified: false,
|
|
178
|
+
image: null,
|
|
179
|
+
role: 'user' as const,
|
|
180
|
+
banned: false,
|
|
181
|
+
banReason: null,
|
|
182
|
+
banExpires: null,
|
|
183
|
+
createdAt: new Date(),
|
|
184
|
+
updatedAt: new Date(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const updatedUser = {
|
|
188
|
+
...mockUser,
|
|
189
|
+
name: 'Updated Name',
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
it('should update user successfully', async () => {
|
|
193
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
|
194
|
+
vi.mocked(prisma.user.update).mockResolvedValue(updatedUser);
|
|
195
|
+
|
|
196
|
+
const user = await service.updateUser('actor-id', 'super_admin', '1', {
|
|
197
|
+
name: 'Updated Name',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(user?.name).toBe('Updated Name');
|
|
201
|
+
expect(user?.id).toBe('1');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should throw NotFoundError when user not found', async () => {
|
|
205
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
|
206
|
+
|
|
207
|
+
await expect(
|
|
208
|
+
service.updateUser('actor-id', 'super_admin', 'non-existent-id', {
|
|
209
|
+
name: 'New Name',
|
|
210
|
+
})
|
|
211
|
+
).rejects.toThrow('User not found');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('deleteUser', () => {
|
|
216
|
+
const mockUser = {
|
|
217
|
+
id: '1',
|
|
218
|
+
email: 'test@example.com',
|
|
219
|
+
name: 'Test User',
|
|
220
|
+
emailVerified: false,
|
|
221
|
+
image: null,
|
|
222
|
+
role: 'user' as const,
|
|
223
|
+
banned: false,
|
|
224
|
+
banReason: null,
|
|
225
|
+
banExpires: null,
|
|
226
|
+
createdAt: new Date(),
|
|
227
|
+
updatedAt: new Date(),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
it('should delete user successfully', async () => {
|
|
231
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
|
232
|
+
vi.mocked(prisma.user.delete).mockResolvedValue(mockUser);
|
|
233
|
+
|
|
234
|
+
await service.deleteUser('actor-id', 'super_admin', '1');
|
|
235
|
+
|
|
236
|
+
expect(prisma.user.delete).toHaveBeenCalledWith({
|
|
237
|
+
where: { id: '1' },
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should throw NotFoundError when user not found', async () => {
|
|
242
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
|
243
|
+
|
|
244
|
+
await expect(
|
|
245
|
+
service.deleteUser('actor-id', 'super_admin', 'non-existent-id')
|
|
246
|
+
).rejects.toThrow('User not found');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PaginatedResponse,
|
|
3
|
+
type QueryUsers,
|
|
4
|
+
} from '@repo/packages-types/pagination';
|
|
5
|
+
import { type Role } from '@repo/packages-types/role';
|
|
6
|
+
import {
|
|
7
|
+
type CreateUser,
|
|
8
|
+
type UpdateUser,
|
|
9
|
+
type User,
|
|
10
|
+
} from '@repo/packages-types/user';
|
|
11
|
+
import { ForbiddenError, NotFoundError } from '@repo/packages-utils/errors';
|
|
12
|
+
|
|
13
|
+
import type { LoggerService } from '@/common/logger.service';
|
|
14
|
+
import type { PrismaClient } from '@/generated/client/client.js';
|
|
15
|
+
import type { AuthorizationService } from '@/services/authorization.service';
|
|
16
|
+
|
|
17
|
+
export class UsersService {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly prisma: PrismaClient,
|
|
20
|
+
private readonly logger: LoggerService,
|
|
21
|
+
private readonly authorizationService: AuthorizationService
|
|
22
|
+
) {
|
|
23
|
+
this.logger.setContext('UsersService');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getUsers(query: QueryUsers): Promise<PaginatedResponse<User>> {
|
|
27
|
+
const where = query.search
|
|
28
|
+
? {
|
|
29
|
+
OR: [
|
|
30
|
+
{ name: { contains: query.search, mode: 'insensitive' as const } },
|
|
31
|
+
{ email: { contains: query.search, mode: 'insensitive' as const } },
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
: undefined;
|
|
35
|
+
|
|
36
|
+
const [users, total] = await Promise.all([
|
|
37
|
+
this.prisma.user.findMany({
|
|
38
|
+
where,
|
|
39
|
+
orderBy: { [query.sortBy]: query.sortOrder },
|
|
40
|
+
skip: (query.page - 1) * query.limit,
|
|
41
|
+
take: query.limit,
|
|
42
|
+
}),
|
|
43
|
+
this.prisma.user.count({ where }),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const totalPages = Math.ceil(total / query.limit);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
data: users as User[],
|
|
50
|
+
pagination: {
|
|
51
|
+
page: query.page,
|
|
52
|
+
limit: query.limit,
|
|
53
|
+
total,
|
|
54
|
+
totalPages,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getUserById(id: string): Promise<User> {
|
|
60
|
+
const user = await this.prisma.user.findUnique({
|
|
61
|
+
where: { id },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!user) {
|
|
65
|
+
this.logger.warn('User not found', { userId: id });
|
|
66
|
+
throw new NotFoundError('User not found', { userId: id });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return user as User;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async createUser(createUser: CreateUser): Promise<User> {
|
|
73
|
+
this.logger.info('Creating user', { email: createUser.email });
|
|
74
|
+
|
|
75
|
+
const user = await this.prisma.user.create({
|
|
76
|
+
data: {
|
|
77
|
+
email: createUser.email,
|
|
78
|
+
name: createUser.name,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.logger.info('User created successfully', { userId: user.id });
|
|
83
|
+
return user as User;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async updateUser(
|
|
87
|
+
actorId: string,
|
|
88
|
+
actorRole: Role,
|
|
89
|
+
targetId: string,
|
|
90
|
+
updateUser: UpdateUser
|
|
91
|
+
): Promise<User> {
|
|
92
|
+
this.logger.info('Updating user', {
|
|
93
|
+
actorId,
|
|
94
|
+
actorRole,
|
|
95
|
+
targetId,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const targetUser = await this.prisma.user.findUnique({
|
|
99
|
+
where: { id: targetId },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!targetUser) {
|
|
103
|
+
this.logger.warn('User not found for update', { userId: targetId });
|
|
104
|
+
throw new NotFoundError('User not found', { userId: targetId });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if actor can modify target user
|
|
108
|
+
this.authorizationService.assertCanModifyUser(
|
|
109
|
+
actorId,
|
|
110
|
+
actorRole,
|
|
111
|
+
targetId,
|
|
112
|
+
targetUser.role as Role
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Check role change permissions
|
|
116
|
+
if (updateUser.role && updateUser.role !== targetUser.role) {
|
|
117
|
+
this.authorizationService.assertCanChangeRole(
|
|
118
|
+
actorId,
|
|
119
|
+
actorRole,
|
|
120
|
+
targetId,
|
|
121
|
+
targetUser.role as Role,
|
|
122
|
+
updateUser.role
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check email change permissions
|
|
127
|
+
if (updateUser.email && updateUser.email !== targetUser.email) {
|
|
128
|
+
this.authorizationService.assertCanChangeEmail(actorRole);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const updatedUser = await this.prisma.user.update({
|
|
132
|
+
where: { id: targetId },
|
|
133
|
+
data: updateUser,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this.logger.info('User updated successfully', {
|
|
137
|
+
actorId,
|
|
138
|
+
targetId,
|
|
139
|
+
changes: Object.keys(updateUser),
|
|
140
|
+
});
|
|
141
|
+
return updatedUser as User;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async deleteUser(
|
|
145
|
+
actorId: string,
|
|
146
|
+
actorRole: Role,
|
|
147
|
+
targetId: string
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
this.logger.info('Deleting user', {
|
|
150
|
+
actorId,
|
|
151
|
+
actorRole,
|
|
152
|
+
targetId,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const targetUser = await this.prisma.user.findUnique({
|
|
156
|
+
where: { id: targetId },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!targetUser) {
|
|
160
|
+
this.logger.warn('User not found for deletion', { userId: targetId });
|
|
161
|
+
throw new NotFoundError('User not found', { userId: targetId });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check if actor can delete target user (includes self-deletion check)
|
|
165
|
+
this.authorizationService.assertCanDeleteUser(
|
|
166
|
+
actorId,
|
|
167
|
+
actorRole,
|
|
168
|
+
targetId,
|
|
169
|
+
targetUser.role as Role
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Prevent deleting the last super_admin
|
|
173
|
+
if (targetUser.role === 'super_admin') {
|
|
174
|
+
const superAdminCount = await this.prisma.user.count({
|
|
175
|
+
where: { role: 'super_admin' },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (superAdminCount <= 1) {
|
|
179
|
+
this.logger.warn('Attempt to delete last super admin', {
|
|
180
|
+
actorId,
|
|
181
|
+
targetId,
|
|
182
|
+
});
|
|
183
|
+
throw new ForbiddenError(
|
|
184
|
+
'Cannot delete the last super admin. Please promote another user to super admin first.'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await this.prisma.user.delete({
|
|
190
|
+
where: { id: targetId },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
this.logger.info('User deleted successfully', {
|
|
194
|
+
actorId,
|
|
195
|
+
targetId,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { MultipartFile } from '@fastify/multipart';
|
|
2
|
+
|
|
3
|
+
export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes
|
|
4
|
+
|
|
5
|
+
export const ALLOWED_MIME_TYPES = {
|
|
6
|
+
// Images
|
|
7
|
+
'image/jpeg': ['.jpg', '.jpeg'],
|
|
8
|
+
'image/png': ['.png'],
|
|
9
|
+
'image/gif': ['.gif'],
|
|
10
|
+
'image/webp': ['.webp'],
|
|
11
|
+
'image/svg+xml': ['.svg'],
|
|
12
|
+
// Documents
|
|
13
|
+
'application/pdf': ['.pdf'],
|
|
14
|
+
'application/msword': ['.doc'],
|
|
15
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [
|
|
16
|
+
'.docx',
|
|
17
|
+
],
|
|
18
|
+
'application/vnd.ms-excel': ['.xls'],
|
|
19
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [
|
|
20
|
+
'.xlsx',
|
|
21
|
+
],
|
|
22
|
+
'text/plain': ['.txt'],
|
|
23
|
+
'text/csv': ['.csv'],
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export type AllowedMimeType = keyof typeof ALLOWED_MIME_TYPES;
|
|
27
|
+
|
|
28
|
+
export interface FileValidationError {
|
|
29
|
+
field: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FileValidationResult {
|
|
34
|
+
valid: boolean;
|
|
35
|
+
error?: FileValidationError;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateFile(file: MultipartFile): FileValidationResult {
|
|
39
|
+
const { mimetype, filename } = file;
|
|
40
|
+
|
|
41
|
+
// Check if MIME type is allowed
|
|
42
|
+
if (!isAllowedMimeType(mimetype)) {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
error: {
|
|
46
|
+
field: 'file',
|
|
47
|
+
message: `File type '${mimetype}' is not allowed. Allowed types: images (JPEG, PNG, GIF, WebP, SVG), documents (PDF, DOC, DOCX, XLS, XLSX, TXT, CSV)`,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate file extension matches MIME type
|
|
53
|
+
const extension = getFileExtension(filename);
|
|
54
|
+
const allowedExtensions = ALLOWED_MIME_TYPES[mimetype as AllowedMimeType];
|
|
55
|
+
|
|
56
|
+
if (!(allowedExtensions as readonly string[]).includes(extension)) {
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
error: {
|
|
60
|
+
field: 'file',
|
|
61
|
+
message: `File extension '${extension}' does not match MIME type '${mimetype}'`,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { valid: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function validateFileSize(
|
|
70
|
+
file: MultipartFile
|
|
71
|
+
): Promise<FileValidationResult> {
|
|
72
|
+
// Note: @fastify/multipart doesn't provide file size directly
|
|
73
|
+
// We need to check size during streaming or after buffering
|
|
74
|
+
// This is handled in the upload handler
|
|
75
|
+
return { valid: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isAllowedMimeType(mimetype: string): boolean {
|
|
79
|
+
return mimetype in ALLOWED_MIME_TYPES;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getFileExtension(filename: string): string {
|
|
83
|
+
const lastDot = filename.lastIndexOf('.');
|
|
84
|
+
return lastDot === -1 ? '' : filename.slice(lastDot).toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isImageFile(mimetype: string): boolean {
|
|
88
|
+
return mimetype.startsWith('image/');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function sanitizeFilename(filename: string): string {
|
|
92
|
+
// Remove path separators and special characters
|
|
93
|
+
return filename
|
|
94
|
+
.replace(/[/\\]/g, '')
|
|
95
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
96
|
+
.slice(0, 255); // Limit filename length
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function generateUniqueFilename(originalFilename: string): string {
|
|
100
|
+
const extension = getFileExtension(originalFilename);
|
|
101
|
+
const timestamp = Date.now();
|
|
102
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
103
|
+
const sanitized = sanitizeFilename(
|
|
104
|
+
originalFilename.replace(extension, '')
|
|
105
|
+
).slice(0, 50);
|
|
106
|
+
|
|
107
|
+
return `${timestamp}-${random}-${sanitized}${extension}`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
echo "🚀 Starting API deployment..."
|
|
5
|
+
|
|
6
|
+
# Wait for database to be ready (handles Railway sleep mode)
|
|
7
|
+
echo "⏳ Waiting for database to wake up..."
|
|
8
|
+
|
|
9
|
+
max_attempts=30
|
|
10
|
+
attempt=0
|
|
11
|
+
|
|
12
|
+
until npx prisma db push --accept-data-loss --skip-generate || [ $attempt -eq $max_attempts ]; do
|
|
13
|
+
attempt=$((attempt + 1))
|
|
14
|
+
echo " Database not ready yet (attempt $attempt/$max_attempts). Retrying in 2 seconds..."
|
|
15
|
+
sleep 2
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
if [ $attempt -eq $max_attempts ]; then
|
|
19
|
+
echo "❌ Database connection failed after $max_attempts attempts"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
echo "✅ Database is ready!"
|
|
24
|
+
|
|
25
|
+
# Run migrations (production-safe, no prompts)
|
|
26
|
+
echo "🔄 Running database migrations..."
|
|
27
|
+
npx prisma migrate deploy
|
|
28
|
+
|
|
29
|
+
echo "✅ Migrations complete!"
|
|
30
|
+
|
|
31
|
+
# Start the API server
|
|
32
|
+
echo "🎯 Starting API server..."
|
|
33
|
+
exec node dist/src/main.js
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
|
|
4
|
+
import { loadEnv } from '@/config/env';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a Fastify test instance with minimal plugins for testing
|
|
8
|
+
*/
|
|
9
|
+
export async function createTestApp(): Promise<FastifyInstance> {
|
|
10
|
+
loadEnv(); // Load test environment variables
|
|
11
|
+
|
|
12
|
+
const app = Fastify({
|
|
13
|
+
logger: false, // Disable logging in tests
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return app;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Close the Fastify test instance
|
|
21
|
+
*/
|
|
22
|
+
export async function closeTestApp(app: FastifyInstance): Promise<void> {
|
|
23
|
+
await app.close();
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { AuthorizationService } from '@/services/authorization.service';
|
|
4
|
+
|
|
5
|
+
export function createMockAuthorizationService(): AuthorizationService {
|
|
6
|
+
return {
|
|
7
|
+
canModifyUser: vi.fn().mockReturnValue(true),
|
|
8
|
+
canDeleteUser: vi.fn().mockReturnValue(true),
|
|
9
|
+
canChangeRole: vi.fn().mockReturnValue(true),
|
|
10
|
+
canChangeEmail: vi.fn().mockReturnValue(true),
|
|
11
|
+
assertCanModifyUser: vi.fn(),
|
|
12
|
+
assertCanDeleteUser: vi.fn(),
|
|
13
|
+
assertCanChangeRole: vi.fn(),
|
|
14
|
+
assertCanChangeEmail: vi.fn(),
|
|
15
|
+
} as unknown as AuthorizationService;
|
|
16
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { LoggerService } from '@/common/logger.service';
|
|
4
|
+
|
|
5
|
+
export function createMockLogger(): LoggerService {
|
|
6
|
+
const mockLogger = {
|
|
7
|
+
setContext: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
error: vi.fn(),
|
|
10
|
+
warn: vi.fn(),
|
|
11
|
+
debug: vi.fn(),
|
|
12
|
+
http: vi.fn(),
|
|
13
|
+
child: vi.fn(),
|
|
14
|
+
minimal: vi.fn(),
|
|
15
|
+
normal: vi.fn(),
|
|
16
|
+
detailed: vi.fn(),
|
|
17
|
+
verbose: vi.fn(),
|
|
18
|
+
} as unknown as LoggerService;
|
|
19
|
+
|
|
20
|
+
// Make verbosity methods chainable
|
|
21
|
+
mockLogger.minimal = vi.fn().mockReturnValue(mockLogger);
|
|
22
|
+
mockLogger.normal = vi.fn().mockReturnValue(mockLogger);
|
|
23
|
+
mockLogger.detailed = vi.fn().mockReturnValue(mockLogger);
|
|
24
|
+
mockLogger.verbose = vi.fn().mockReturnValue(mockLogger);
|
|
25
|
+
mockLogger.child = vi.fn().mockReturnValue(mockLogger);
|
|
26
|
+
|
|
27
|
+
return mockLogger;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { PrismaClient } from '@/generated/client/client.js';
|
|
4
|
+
|
|
5
|
+
export function createMockPrisma(): PrismaClient {
|
|
6
|
+
return {
|
|
7
|
+
user: {
|
|
8
|
+
findMany: vi.fn(),
|
|
9
|
+
findUnique: vi.fn(),
|
|
10
|
+
findFirst: vi.fn(),
|
|
11
|
+
create: vi.fn(),
|
|
12
|
+
update: vi.fn(),
|
|
13
|
+
delete: vi.fn(),
|
|
14
|
+
count: vi.fn(),
|
|
15
|
+
},
|
|
16
|
+
session: {
|
|
17
|
+
findMany: vi.fn(),
|
|
18
|
+
findUnique: vi.fn(),
|
|
19
|
+
findFirst: vi.fn(),
|
|
20
|
+
create: vi.fn(),
|
|
21
|
+
update: vi.fn(),
|
|
22
|
+
delete: vi.fn(),
|
|
23
|
+
deleteMany: vi.fn(),
|
|
24
|
+
count: vi.fn(),
|
|
25
|
+
},
|
|
26
|
+
$connect: vi.fn(),
|
|
27
|
+
$disconnect: vi.fn(),
|
|
28
|
+
$queryRaw: vi.fn(),
|
|
29
|
+
} as unknown as PrismaClient;
|
|
30
|
+
}
|