@odvi/create-dtt-framework 0.1.3 → 0.1.6

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 (111) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +16 -13
  3. package/dist/commands/create.js.map +1 -1
  4. package/package.json +3 -2
  5. package/template/.env.example +106 -0
  6. package/template/components.json +22 -0
  7. package/template/docs/framework/01-overview.md +289 -0
  8. package/template/docs/framework/02-techstack.md +503 -0
  9. package/template/docs/framework/api-layer.md +681 -0
  10. package/template/docs/framework/clerk-authentication.md +649 -0
  11. package/template/docs/framework/cli-installation.md +564 -0
  12. package/template/docs/framework/deployment/ci-cd.md +907 -0
  13. package/template/docs/framework/deployment/digitalocean.md +991 -0
  14. package/template/docs/framework/deployment/domain-setup.md +972 -0
  15. package/template/docs/framework/deployment/environment-variables.md +862 -0
  16. package/template/docs/framework/deployment/monitoring.md +927 -0
  17. package/template/docs/framework/deployment/production-checklist.md +649 -0
  18. package/template/docs/framework/deployment/vercel.md +791 -0
  19. package/template/docs/framework/environment-variables.md +646 -0
  20. package/template/docs/framework/health-check-system.md +583 -0
  21. package/template/docs/framework/implementation.md +559 -0
  22. package/template/docs/framework/snowflake-integration.md +594 -0
  23. package/template/docs/framework/state-management.md +615 -0
  24. package/template/docs/framework/supabase-integration.md +582 -0
  25. package/template/docs/framework/testing-guide.md +544 -0
  26. package/template/docs/framework/what-did-i-miss.md +526 -0
  27. package/template/drizzle.config.ts +11 -0
  28. package/template/next.config.js +21 -0
  29. package/template/postcss.config.js +5 -0
  30. package/template/prettier.config.js +4 -0
  31. package/template/public/favicon.ico +0 -0
  32. package/template/src/app/(auth)/layout.tsx +4 -0
  33. package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
  34. package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
  35. package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
  36. package/template/src/app/(dashboard)/health/page.tsx +16 -0
  37. package/template/src/app/(dashboard)/layout.tsx +17 -0
  38. package/template/src/app/api/[[...route]]/route.ts +11 -0
  39. package/template/src/app/api/debug-files/route.ts +33 -0
  40. package/template/src/app/api/webhooks/clerk/route.ts +112 -0
  41. package/template/src/app/layout.tsx +28 -0
  42. package/template/src/app/page.tsx +12 -0
  43. package/template/src/app/providers.tsx +20 -0
  44. package/template/src/components/layouts/navbar.tsx +14 -0
  45. package/template/src/components/shared/loading-spinner.tsx +6 -0
  46. package/template/src/components/ui/badge.tsx +46 -0
  47. package/template/src/components/ui/button.tsx +62 -0
  48. package/template/src/components/ui/card.tsx +92 -0
  49. package/template/src/components/ui/collapsible.tsx +33 -0
  50. package/template/src/components/ui/scroll-area.tsx +58 -0
  51. package/template/src/components/ui/sheet.tsx +139 -0
  52. package/template/src/config/__tests__/env.test.ts +164 -0
  53. package/template/src/config/__tests__/site.test.ts +46 -0
  54. package/template/src/config/env.ts +36 -0
  55. package/template/src/config/site.ts +10 -0
  56. package/template/src/env.js +44 -0
  57. package/template/src/features/__tests__/health-check-config.test.ts +142 -0
  58. package/template/src/features/__tests__/health-check-types.test.ts +201 -0
  59. package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
  60. package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
  61. package/template/src/features/documentation/index.tsx +92 -0
  62. package/template/src/features/documentation/utils/doc-loader.ts +177 -0
  63. package/template/src/features/health-check/components/health-dashboard.tsx +374 -0
  64. package/template/src/features/health-check/config.ts +71 -0
  65. package/template/src/features/health-check/index.ts +4 -0
  66. package/template/src/features/health-check/stores/health-store.ts +14 -0
  67. package/template/src/features/health-check/types.ts +18 -0
  68. package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
  69. package/template/src/hooks/queries/use-health-checks.ts +16 -0
  70. package/template/src/hooks/utils/use-debounce.ts +20 -0
  71. package/template/src/lib/__tests__/utils.test.ts +52 -0
  72. package/template/src/lib/__tests__/validators.test.ts +114 -0
  73. package/template/src/lib/nextbank/client.ts +67 -0
  74. package/template/src/lib/snowflake/client.ts +102 -0
  75. package/template/src/lib/supabase/admin.ts +7 -0
  76. package/template/src/lib/supabase/client.ts +7 -0
  77. package/template/src/lib/supabase/server.ts +23 -0
  78. package/template/src/lib/utils.ts +6 -0
  79. package/template/src/lib/validators.ts +9 -0
  80. package/template/src/middleware.ts +22 -0
  81. package/template/src/server/api/index.ts +22 -0
  82. package/template/src/server/api/middleware/auth.ts +19 -0
  83. package/template/src/server/api/middleware/logger.ts +4 -0
  84. package/template/src/server/api/routes/health/clerk.ts +214 -0
  85. package/template/src/server/api/routes/health/database.ts +141 -0
  86. package/template/src/server/api/routes/health/edge-functions.ts +107 -0
  87. package/template/src/server/api/routes/health/framework.ts +48 -0
  88. package/template/src/server/api/routes/health/index.ts +102 -0
  89. package/template/src/server/api/routes/health/nextbank.ts +46 -0
  90. package/template/src/server/api/routes/health/snowflake.ts +83 -0
  91. package/template/src/server/api/routes/health/storage.ts +177 -0
  92. package/template/src/server/api/routes/users.ts +79 -0
  93. package/template/src/server/db/index.ts +17 -0
  94. package/template/src/server/db/queries/users.ts +8 -0
  95. package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
  96. package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
  97. package/template/src/server/db/schema/health-checks.ts +11 -0
  98. package/template/src/server/db/schema/index.ts +2 -0
  99. package/template/src/server/db/schema/users.ts +16 -0
  100. package/template/src/server/db/schema.ts +1 -0
  101. package/template/src/stores/__tests__/ui-store.test.ts +87 -0
  102. package/template/src/stores/ui-store.ts +14 -0
  103. package/template/src/styles/globals.css +129 -0
  104. package/template/src/test/mocks/clerk.ts +35 -0
  105. package/template/src/test/mocks/snowflake.ts +28 -0
  106. package/template/src/test/mocks/supabase.ts +37 -0
  107. package/template/src/test/setup.ts +69 -0
  108. package/template/src/test/utils/test-helpers.ts +158 -0
  109. package/template/src/types/index.ts +14 -0
  110. package/template/tsconfig.json +43 -0
  111. package/template/vitest.config.ts +44 -0
@@ -0,0 +1,164 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { z } from 'zod';
3
+
4
+ // Mock process.env
5
+ const originalEnv = process.env;
6
+
7
+ describe('env configuration', () => {
8
+ beforeEach(() => {
9
+ // Reset process.env before each test
10
+ process.env = { ...originalEnv };
11
+ });
12
+
13
+ afterEach(() => {
14
+ process.env = originalEnv;
15
+ });
16
+
17
+ it('should have valid environment schema structure', () => {
18
+ // Import after setting up env
19
+ const envSchema = z.object({
20
+ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
21
+ NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
22
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().startsWith('pk_').default('pk_test_placeholder'),
23
+ CLERK_SECRET_KEY: z.string().startsWith('sk_').default('sk_test_placeholder'),
24
+ CLERK_WEBHOOK_SECRET: z.string().optional(),
25
+ NEXT_PUBLIC_SUPABASE_URL: z.string().url().default('https://placeholder.supabase.co'),
26
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().default('placeholder'),
27
+ SUPABASE_SERVICE_ROLE_KEY: z.string().default('placeholder'),
28
+ DATABASE_URL: z.string().default('postgresql://postgres:password@localhost:5432/postgres'),
29
+ SNOWFLAKE_ACCOUNT: z.string().default('placeholder'),
30
+ SNOWFLAKE_USERNAME: z.string().default('placeholder'),
31
+ SNOWFLAKE_PASSWORD: z.string().default('placeholder'),
32
+ SNOWFLAKE_WAREHOUSE: z.string().default('COMPUTE_WH'),
33
+ SNOWFLAKE_DATABASE: z.string().default('ANALYTICS'),
34
+ SNOWFLAKE_SCHEMA: z.string().default('PUBLIC'),
35
+ SNOWFLAKE_ROLE: z.string().default('ANALYST'),
36
+ NEXTBANK_API: z.string().url().optional(),
37
+ NEXTBANK_API_USERNAME: z.string().optional(),
38
+ NEXTBANK_API_PASSWORD: z.string().optional(),
39
+ });
40
+
41
+ const result = envSchema.safeParse({});
42
+ expect(result.success).toBe(true);
43
+ });
44
+
45
+ it('should validate NODE_ENV enum values', () => {
46
+ const validEnvs = ['development', 'test', 'production'] as const;
47
+ const envSchema = z.object({
48
+ NODE_ENV: z.enum(validEnvs).default('development'),
49
+ });
50
+
51
+ validEnvs.forEach((env) => {
52
+ const result = envSchema.safeParse({ NODE_ENV: env });
53
+ expect(result.success).toBe(true);
54
+ });
55
+ });
56
+
57
+ it('should reject invalid NODE_ENV values', () => {
58
+ const envSchema = z.object({
59
+ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
60
+ });
61
+
62
+ const result = envSchema.safeParse({ NODE_ENV: 'invalid' });
63
+ expect(result.success).toBe(false);
64
+ });
65
+
66
+ it('should validate URL fields', () => {
67
+ const urlSchema = z.object({
68
+ NEXT_PUBLIC_APP_URL: z.string().url(),
69
+ NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
70
+ });
71
+
72
+ const validResult = urlSchema.safeParse({
73
+ NEXT_PUBLIC_APP_URL: 'https://example.com',
74
+ NEXT_PUBLIC_SUPABASE_URL: 'https://supabase.example.com',
75
+ });
76
+ expect(validResult.success).toBe(true);
77
+
78
+ const invalidResult = urlSchema.safeParse({
79
+ NEXT_PUBLIC_APP_URL: 'not-a-url',
80
+ NEXT_PUBLIC_SUPABASE_URL: 'also-not-a-url',
81
+ });
82
+ expect(invalidResult.success).toBe(false);
83
+ });
84
+
85
+ it('should validate Clerk key prefixes', () => {
86
+ const clerkSchema = z.object({
87
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
88
+ CLERK_SECRET_KEY: z.string().startsWith('sk_'),
89
+ });
90
+
91
+ const validResult = clerkSchema.safeParse({
92
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 'pk_test_123',
93
+ CLERK_SECRET_KEY: 'sk_test_456',
94
+ });
95
+ expect(validResult.success).toBe(true);
96
+
97
+ const invalidResult = clerkSchema.safeParse({
98
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 'invalid_key',
99
+ CLERK_SECRET_KEY: 'invalid_secret',
100
+ });
101
+ expect(invalidResult.success).toBe(false);
102
+ });
103
+
104
+ it('should allow optional NextBank fields', () => {
105
+ const nextbankSchema = z.object({
106
+ NEXTBANK_API: z.string().url().optional(),
107
+ NEXTBANK_API_USERNAME: z.string().optional(),
108
+ NEXTBANK_API_PASSWORD: z.string().optional(),
109
+ });
110
+
111
+ const result = nextbankSchema.safeParse({});
112
+ expect(result.success).toBe(true);
113
+ });
114
+
115
+ it('should validate Snowflake configuration fields', () => {
116
+ const snowflakeSchema = z.object({
117
+ SNOWFLAKE_ACCOUNT: z.string(),
118
+ SNOWFLAKE_USERNAME: z.string(),
119
+ SNOWFLAKE_PASSWORD: z.string(),
120
+ SNOWFLAKE_WAREHOUSE: z.string(),
121
+ SNOWFLAKE_DATABASE: z.string(),
122
+ SNOWFLAKE_SCHEMA: z.string(),
123
+ SNOWFLAKE_ROLE: z.string(),
124
+ });
125
+
126
+ const result = snowflakeSchema.safeParse({
127
+ SNOWFLAKE_ACCOUNT: 'account',
128
+ SNOWFLAKE_USERNAME: 'user',
129
+ SNOWFLAKE_PASSWORD: 'pass',
130
+ SNOWFLAKE_WAREHOUSE: 'WH',
131
+ SNOWFLAKE_DATABASE: 'DB',
132
+ SNOWFLAKE_SCHEMA: 'SCHEMA',
133
+ SNOWFLAKE_ROLE: 'ROLE',
134
+ });
135
+ expect(result.success).toBe(true);
136
+ });
137
+
138
+ it('should use default values when environment variables are not set', () => {
139
+ const envSchema = z.object({
140
+ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
141
+ NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
142
+ });
143
+
144
+ const result = envSchema.parse({});
145
+ expect(result.NODE_ENV).toBe('development');
146
+ expect(result.NEXT_PUBLIC_APP_URL).toBe('http://localhost:3000');
147
+ });
148
+
149
+ it('should override defaults with environment variables', () => {
150
+ const testEnv = {
151
+ NODE_ENV: 'production',
152
+ NEXT_PUBLIC_APP_URL: 'https://production.example.com',
153
+ };
154
+
155
+ const envSchema = z.object({
156
+ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
157
+ NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
158
+ });
159
+
160
+ const result = envSchema.parse(testEnv);
161
+ expect(result.NODE_ENV).toBe('production');
162
+ expect(result.NEXT_PUBLIC_APP_URL).toBe('https://production.example.com');
163
+ });
164
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('siteConfig', () => {
4
+ it('should have a name property', () => {
5
+ const { siteConfig } = require('../site.ts');
6
+ expect(siteConfig.name).toBe('DTT Framework');
7
+ });
8
+
9
+ it('should have a description property', () => {
10
+ const { siteConfig } = require('../site.ts');
11
+ expect(siteConfig.description).toBe('Production-ready Next.js boilerplate with integrated services');
12
+ });
13
+
14
+ it('should have a url property', () => {
15
+ const { siteConfig } = require('../site.ts');
16
+ expect(siteConfig.url).toBeDefined();
17
+ expect(typeof siteConfig.url).toBe('string');
18
+ });
19
+
20
+ it('should have a links object', () => {
21
+ const { siteConfig } = require('../site.ts');
22
+ expect(siteConfig.links).toBeDefined();
23
+ expect(typeof siteConfig.links).toBe('object');
24
+ });
25
+
26
+ it('should have a twitter link', () => {
27
+ const { siteConfig } = require('../site.ts');
28
+ expect(siteConfig.links.twitter).toBe('https://twitter.com/odvi');
29
+ });
30
+
31
+ it('should have a github link', () => {
32
+ const { siteConfig } = require('../site.ts');
33
+ expect(siteConfig.links.github).toBe('https://github.com/odvi');
34
+ });
35
+
36
+ it('should have a valid URL format for url property', () => {
37
+ const { siteConfig } = require('../site.ts');
38
+ expect(siteConfig.url).toMatch(/^https?:\/\/.+/);
39
+ });
40
+
41
+ it('should have valid URL formats for all links', () => {
42
+ const { siteConfig } = require('../site.ts');
43
+ expect(siteConfig.links.twitter).toMatch(/^https?:\/\/.+/);
44
+ expect(siteConfig.links.github).toMatch(/^https?:\/\/.+/);
45
+ });
46
+ });
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod'
2
+
3
+ const envSchema = z.object({
4
+ // App
5
+ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
6
+ NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
7
+
8
+ // Clerk
9
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().startsWith('pk_').default('pk_test_placeholder'),
10
+ CLERK_SECRET_KEY: z.string().startsWith('sk_').default('sk_test_placeholder'),
11
+ CLERK_WEBHOOK_SECRET: z.string().optional(),
12
+
13
+ // Supabase
14
+ NEXT_PUBLIC_SUPABASE_URL: z.string().url().default('https://placeholder.supabase.co'),
15
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().default('placeholder'),
16
+ SUPABASE_SERVICE_ROLE_KEY: z.string().default('placeholder'),
17
+ DATABASE_URL: z.string().default('postgresql://postgres:password@localhost:5432/postgres'),
18
+
19
+ // Snowflake
20
+ SNOWFLAKE_ACCOUNT: z.string().default('placeholder'),
21
+ SNOWFLAKE_USERNAME: z.string().default('placeholder'),
22
+ SNOWFLAKE_AUTHENTICATOR: z.literal('SNOWFLAKE_JWT').default('SNOWFLAKE_JWT'),
23
+ SNOWFLAKE_PRIVATE_KEY: z.string().default('placeholder'),
24
+ SNOWFLAKE_PRIVATE_KEY_PASSPHRASE: z.string().optional(),
25
+ SNOWFLAKE_WAREHOUSE: z.string().default('COMPUTE_WH'),
26
+ SNOWFLAKE_ROLE: z.string().default('ACCOUNTADMIN'),
27
+ SNOWFLAKE_LOGGING: z.string().transform((val) => val === 'true').optional(),
28
+
29
+ // NextBank (optional)
30
+ NEXTBANK_API: z.string().url().optional(),
31
+ NEXTBANK_API_USERNAME: z.string().optional(),
32
+ NEXTBANK_API_PASSWORD: z.string().optional(),
33
+ })
34
+
35
+ export const env = envSchema.parse(process.env)
36
+ export type Env = z.infer<typeof envSchema>
@@ -0,0 +1,10 @@
1
+ // Site configuration placeholder
2
+ export const siteConfig = {
3
+ name: 'DTT Framework',
4
+ description: 'Production-ready Next.js boilerplate with integrated services',
5
+ url: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
6
+ links: {
7
+ twitter: 'https://twitter.com/odvi',
8
+ github: 'https://github.com/odvi',
9
+ },
10
+ }
@@ -0,0 +1,44 @@
1
+ import { createEnv } from "@t3-oss/env-nextjs";
2
+ import { z } from "zod";
3
+
4
+ export const env = createEnv({
5
+ /**
6
+ * Specify your server-side environment variables schema here. This way you can ensure the app
7
+ * isn't built with invalid env vars.
8
+ */
9
+ server: {
10
+ DATABASE_URL: z.string().url(),
11
+ NODE_ENV: z
12
+ .enum(["development", "test", "production"])
13
+ .default("development"),
14
+ },
15
+
16
+ /**
17
+ * Specify your client-side environment variables schema here. This way you can ensure the app
18
+ * isn't built with invalid env vars. To expose them to the client, prefix them with
19
+ * `NEXT_PUBLIC_`.
20
+ */
21
+ client: {
22
+ // NEXT_PUBLIC_CLIENTVAR: z.string(),
23
+ },
24
+
25
+ /**
26
+ * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
27
+ * middlewares) or client-side so we need to destruct manually.
28
+ */
29
+ runtimeEnv: {
30
+ DATABASE_URL: process.env.DATABASE_URL,
31
+ NODE_ENV: process.env.NODE_ENV,
32
+ // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
33
+ },
34
+ /**
35
+ * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
36
+ * useful for Docker builds.
37
+ */
38
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
39
+ /**
40
+ * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
41
+ * `SOME_VAR=''` will throw an error.
42
+ */
43
+ emptyStringAsUndefined: true,
44
+ });
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SERVICES } from '../health-check/config';
3
+
4
+ describe('SERVICES configuration', () => {
5
+ it('should be defined', () => {
6
+ expect(SERVICES).toBeDefined();
7
+ expect(Array.isArray(SERVICES)).toBe(true);
8
+ });
9
+
10
+ it('should have 6 services configured', () => {
11
+ expect(SERVICES).toHaveLength(6);
12
+ });
13
+
14
+ it('should have Clerk Authentication service', () => {
15
+ const clerkService = SERVICES.find((s) => s.name === 'Clerk Authentication');
16
+ expect(clerkService).toBeDefined();
17
+ expect(clerkService?.icon).toBe('key');
18
+ expect(clerkService?.checks).toHaveLength(3);
19
+ });
20
+
21
+ it('should have Supabase Database service', () => {
22
+ const dbService = SERVICES.find((s) => s.name === 'Supabase Database');
23
+ expect(dbService).toBeDefined();
24
+ expect(dbService?.icon).toBe('database');
25
+ expect(dbService?.checks).toHaveLength(3);
26
+ });
27
+
28
+ it('should have Supabase Storage service', () => {
29
+ const storageService = SERVICES.find((s) => s.name === 'Supabase Storage');
30
+ expect(storageService).toBeDefined();
31
+ expect(storageService?.icon).toBe('folder');
32
+ expect(storageService?.checks).toHaveLength(3);
33
+ });
34
+
35
+ it('should have Supabase Edge Functions service', () => {
36
+ const edgeService = SERVICES.find((s) => s.name === 'Supabase Edge Functions');
37
+ expect(edgeService).toBeDefined();
38
+ expect(edgeService?.icon).toBe('zap');
39
+ expect(edgeService?.checks).toHaveLength(2);
40
+ });
41
+
42
+ it('should have Snowflake service', () => {
43
+ const snowflakeService = SERVICES.find((s) => s.name === 'Snowflake');
44
+ expect(snowflakeService).toBeDefined();
45
+ expect(snowflakeService?.icon).toBe('snowflake');
46
+ expect(snowflakeService?.checks).toHaveLength(2);
47
+ });
48
+
49
+ it('should have NextBank service', () => {
50
+ const nextbankService = SERVICES.find((s) => s.name === 'NextBank');
51
+ expect(nextbankService).toBeDefined();
52
+ expect(nextbankService?.icon).toBe('building');
53
+ expect(nextbankService?.checks).toHaveLength(2);
54
+ });
55
+
56
+ it('should have all services with required properties', () => {
57
+ SERVICES.forEach((service) => {
58
+ expect(service.name).toBeDefined();
59
+ expect(typeof service.name).toBe('string');
60
+ expect(service.name.length).toBeGreaterThan(0);
61
+
62
+ expect(service.icon).toBeDefined();
63
+ expect(typeof service.icon).toBe('string');
64
+ expect(service.icon.length).toBeGreaterThan(0);
65
+
66
+ expect(service.checks).toBeDefined();
67
+ expect(Array.isArray(service.checks)).toBe(true);
68
+ expect(service.checks.length).toBeGreaterThan(0);
69
+ });
70
+ });
71
+
72
+ it('should have all checks with required properties', () => {
73
+ SERVICES.forEach((service) => {
74
+ service.checks.forEach((check) => {
75
+ expect(check.name).toBeDefined();
76
+ expect(typeof check.name).toBe('string');
77
+ expect(check.name.length).toBeGreaterThan(0);
78
+
79
+ expect(check.endpoint).toBeDefined();
80
+ expect(typeof check.endpoint).toBe('string');
81
+ expect(check.endpoint.length).toBeGreaterThan(0);
82
+ expect(check.endpoint).toMatch(/^\/.+/);
83
+ });
84
+ });
85
+ });
86
+
87
+ it('should have unique service names', () => {
88
+ const serviceNames = SERVICES.map((s) => s.name);
89
+ const uniqueNames = new Set(serviceNames);
90
+ expect(uniqueNames.size).toBe(serviceNames.length);
91
+ });
92
+
93
+ it('should have unique icons', () => {
94
+ const icons = SERVICES.map((s) => s.icon);
95
+ const uniqueIcons = new Set(icons);
96
+ expect(uniqueIcons.size).toBe(icons.length);
97
+ });
98
+
99
+ it('should have Clerk Authentication checks with correct endpoints', () => {
100
+ const clerkService = SERVICES.find((s) => s.name === 'Clerk Authentication');
101
+ expect(clerkService?.checks[0].endpoint).toBe('/clerk/user');
102
+ expect(clerkService?.checks[1].endpoint).toBe('/clerk/org');
103
+ expect(clerkService?.checks[2].endpoint).toBe('/clerk/members');
104
+ });
105
+
106
+ it('should have Supabase Database checks with correct endpoints', () => {
107
+ const dbService = SERVICES.find((s) => s.name === 'Supabase Database');
108
+ expect(dbService?.checks[0].endpoint).toBe('/database/write');
109
+ expect(dbService?.checks[1].endpoint).toBe('/database/read');
110
+ expect(dbService?.checks[2].endpoint).toBe('/database/delete');
111
+ });
112
+
113
+ it('should have Supabase Storage checks with correct endpoints', () => {
114
+ const storageService = SERVICES.find((s) => s.name === 'Supabase Storage');
115
+ expect(storageService?.checks[0].endpoint).toBe('/storage/upload');
116
+ expect(storageService?.checks[1].endpoint).toBe('/storage/download');
117
+ expect(storageService?.checks[2].endpoint).toBe('/storage/delete');
118
+ });
119
+
120
+ it('should have Edge Functions checks with correct endpoints', () => {
121
+ const edgeService = SERVICES.find((s) => s.name === 'Supabase Edge Functions');
122
+ expect(edgeService?.checks[0].endpoint).toBe('/edge/ping');
123
+ expect(edgeService?.checks[1].endpoint).toBe('/edge/auth');
124
+ });
125
+
126
+ it('should have Snowflake checks with correct endpoints', () => {
127
+ const snowflakeService = SERVICES.find((s) => s.name === 'Snowflake');
128
+ expect(snowflakeService?.checks[0].endpoint).toBe('/snowflake/connect');
129
+ expect(snowflakeService?.checks[1].endpoint).toBe('/snowflake/query');
130
+ });
131
+
132
+ it('should have NextBank checks with correct endpoints', () => {
133
+ const nextbankService = SERVICES.find((s) => s.name === 'NextBank');
134
+ expect(nextbankService?.checks[0].endpoint).toBe('/nextbank/ping');
135
+ expect(nextbankService?.checks[1].endpoint).toBe('/nextbank/auth');
136
+ });
137
+
138
+ it('should have total of 15 checks across all services', () => {
139
+ const totalChecks = SERVICES.reduce((sum, service) => sum + service.checks.length, 0);
140
+ expect(totalChecks).toBe(15);
141
+ });
142
+ });
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { HealthStatus, ServiceCheck, ServiceHealth } from '../health-check/types';
3
+
4
+ describe('Health check types', () => {
5
+ describe('HealthStatus type', () => {
6
+ it('should accept valid health status values', () => {
7
+ const validStatuses: HealthStatus[] = [
8
+ 'healthy',
9
+ 'unhealthy',
10
+ 'error',
11
+ 'pending',
12
+ 'unconfigured',
13
+ ];
14
+
15
+ validStatuses.forEach((status) => {
16
+ expect(status).toBeDefined();
17
+ expect(typeof status).toBe('string');
18
+ });
19
+ });
20
+
21
+ it('should have exactly 5 valid health status values', () => {
22
+ const validStatuses: HealthStatus[] = [
23
+ 'healthy',
24
+ 'unhealthy',
25
+ 'error',
26
+ 'pending',
27
+ 'unconfigured',
28
+ ];
29
+
30
+ expect(validStatuses).toHaveLength(5);
31
+ });
32
+
33
+ it('should allow creating objects with HealthStatus', () => {
34
+ const status: HealthStatus = 'healthy';
35
+ expect(status).toBe('healthy');
36
+ });
37
+ });
38
+
39
+ describe('ServiceCheck interface', () => {
40
+ it('should create a valid ServiceCheck object', () => {
41
+ const serviceCheck: ServiceCheck = {
42
+ name: 'Test Check',
43
+ endpoint: '/test/endpoint',
44
+ status: 'healthy',
45
+ responseTimeMs: 100,
46
+ error: undefined,
47
+ };
48
+
49
+ expect(serviceCheck.name).toBe('Test Check');
50
+ expect(serviceCheck.endpoint).toBe('/test/endpoint');
51
+ expect(serviceCheck.status).toBe('healthy');
52
+ expect(serviceCheck.responseTimeMs).toBe(100);
53
+ expect(serviceCheck.error).toBeUndefined();
54
+ });
55
+
56
+ it('should create ServiceCheck without optional fields', () => {
57
+ const serviceCheck: ServiceCheck = {
58
+ name: 'Test Check',
59
+ endpoint: '/test/endpoint',
60
+ status: 'healthy',
61
+ };
62
+
63
+ expect(serviceCheck.name).toBe('Test Check');
64
+ expect(serviceCheck.endpoint).toBe('/test/endpoint');
65
+ expect(serviceCheck.status).toBe('healthy');
66
+ expect(serviceCheck.responseTimeMs).toBeUndefined();
67
+ expect(serviceCheck.error).toBeUndefined();
68
+ });
69
+
70
+ it('should create ServiceCheck with error field', () => {
71
+ const serviceCheck: ServiceCheck = {
72
+ name: 'Test Check',
73
+ endpoint: '/test/endpoint',
74
+ status: 'error',
75
+ error: 'Connection failed',
76
+ };
77
+
78
+ expect(serviceCheck.status).toBe('error');
79
+ expect(serviceCheck.error).toBe('Connection failed');
80
+ });
81
+
82
+ it('should accept all valid HealthStatus values in ServiceCheck', () => {
83
+ const validStatuses: HealthStatus[] = [
84
+ 'healthy',
85
+ 'unhealthy',
86
+ 'error',
87
+ 'pending',
88
+ 'unconfigured',
89
+ ];
90
+
91
+ validStatuses.forEach((status) => {
92
+ const serviceCheck: ServiceCheck = {
93
+ name: 'Test Check',
94
+ endpoint: '/test/endpoint',
95
+ status,
96
+ };
97
+ expect(serviceCheck.status).toBe(status);
98
+ });
99
+ });
100
+ });
101
+
102
+ describe('ServiceHealth interface', () => {
103
+ it('should create a valid ServiceHealth object', () => {
104
+ const checks: ServiceCheck[] = [
105
+ {
106
+ name: 'Check 1',
107
+ endpoint: '/check1',
108
+ status: 'healthy',
109
+ responseTimeMs: 50,
110
+ },
111
+ {
112
+ name: 'Check 2',
113
+ endpoint: '/check2',
114
+ status: 'healthy',
115
+ responseTimeMs: 75,
116
+ },
117
+ ];
118
+
119
+ const serviceHealth: ServiceHealth = {
120
+ name: 'Test Service',
121
+ icon: 'test-icon',
122
+ status: 'healthy',
123
+ responseTimeMs: 125,
124
+ checks,
125
+ };
126
+
127
+ expect(serviceHealth.name).toBe('Test Service');
128
+ expect(serviceHealth.icon).toBe('test-icon');
129
+ expect(serviceHealth.status).toBe('healthy');
130
+ expect(serviceHealth.responseTimeMs).toBe(125);
131
+ expect(serviceHealth.checks).toHaveLength(2);
132
+ });
133
+
134
+ it('should create ServiceHealth with empty checks array', () => {
135
+ const serviceHealth: ServiceHealth = {
136
+ name: 'Test Service',
137
+ icon: 'test-icon',
138
+ status: 'unconfigured',
139
+ responseTimeMs: 0,
140
+ checks: [],
141
+ };
142
+
143
+ expect(serviceHealth.checks).toHaveLength(0);
144
+ });
145
+
146
+ it('should create ServiceHealth with error status', () => {
147
+ const checks: ServiceCheck[] = [
148
+ {
149
+ name: 'Check 1',
150
+ endpoint: '/check1',
151
+ status: 'error',
152
+ error: 'Failed to connect',
153
+ },
154
+ ];
155
+
156
+ const serviceHealth: ServiceHealth = {
157
+ name: 'Test Service',
158
+ icon: 'test-icon',
159
+ status: 'error',
160
+ responseTimeMs: 0,
161
+ checks,
162
+ };
163
+
164
+ expect(serviceHealth.status).toBe('error');
165
+ expect(serviceHealth.checks[0]?.error).toBe('Failed to connect');
166
+ });
167
+
168
+ it('should allow all valid HealthStatus values in ServiceHealth', () => {
169
+ const validStatuses: HealthStatus[] = [
170
+ 'healthy',
171
+ 'unhealthy',
172
+ 'error',
173
+ 'pending',
174
+ 'unconfigured',
175
+ ];
176
+
177
+ validStatuses.forEach((status) => {
178
+ const serviceHealth: ServiceHealth = {
179
+ name: 'Test Service',
180
+ icon: 'test-icon',
181
+ status,
182
+ responseTimeMs: 0,
183
+ checks: [],
184
+ };
185
+ expect(serviceHealth.status).toBe(status);
186
+ });
187
+ });
188
+
189
+ it('should have responseTimeMs as required field', () => {
190
+ const serviceHealth: ServiceHealth = {
191
+ name: 'Test Service',
192
+ icon: 'test-icon',
193
+ status: 'healthy',
194
+ responseTimeMs: 100,
195
+ checks: [],
196
+ };
197
+
198
+ expect(typeof serviceHealth.responseTimeMs).toBe('number');
199
+ });
200
+ });
201
+ });