@tuturuuu/utils 0.0.3 → 0.6.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.
Files changed (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +120 -1
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,210 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { extractMentions } from '../notification-service';
3
+
4
+ describe('extractMentions', () => {
5
+ describe('basic functionality', () => {
6
+ it('should return empty array for empty string', () => {
7
+ expect(extractMentions('')).toEqual([]);
8
+ });
9
+
10
+ it('should return empty array for null/undefined', () => {
11
+ expect(extractMentions(null as unknown as string)).toEqual([]);
12
+ expect(extractMentions(undefined as unknown as string)).toEqual([]);
13
+ });
14
+
15
+ it('should return empty array for text without mentions', () => {
16
+ expect(extractMentions('Hello World')).toEqual([]);
17
+ expect(extractMentions('No mentions here')).toEqual([]);
18
+ });
19
+ });
20
+
21
+ describe('UUID mention extraction', () => {
22
+ it('should extract a single UUID mention', () => {
23
+ const text =
24
+ 'Hey @[550e8400-e29b-41d4-a716-446655440000], check this out';
25
+ const result = extractMentions(text);
26
+ expect(result).toEqual(['550e8400-e29b-41d4-a716-446655440000']);
27
+ });
28
+
29
+ it('should extract multiple UUID mentions', () => {
30
+ const text = `
31
+ @[550e8400-e29b-41d4-a716-446655440000] and
32
+ @[6ba7b810-9dad-11d1-80b4-00c04fd430c8] please review
33
+ `;
34
+ const result = extractMentions(text);
35
+ expect(result).toHaveLength(2);
36
+ expect(result).toContain('550e8400-e29b-41d4-a716-446655440000');
37
+ expect(result).toContain('6ba7b810-9dad-11d1-80b4-00c04fd430c8');
38
+ });
39
+
40
+ it('should deduplicate repeated mentions', () => {
41
+ const text = `
42
+ @[550e8400-e29b-41d4-a716-446655440000] first mention
43
+ @[550e8400-e29b-41d4-a716-446655440000] second mention
44
+ @[550e8400-e29b-41d4-a716-446655440000] third mention
45
+ `;
46
+ const result = extractMentions(text);
47
+ expect(result).toHaveLength(1);
48
+ expect(result).toEqual(['550e8400-e29b-41d4-a716-446655440000']);
49
+ });
50
+
51
+ it('should handle UUID with uppercase letters', () => {
52
+ const text = '@[550E8400-E29B-41D4-A716-446655440000] uppercase UUID';
53
+ const result = extractMentions(text);
54
+ expect(result).toHaveLength(1);
55
+ // UUIDs should be extracted as-is (case-insensitive matching)
56
+ });
57
+
58
+ it('should handle mixed case UUIDs', () => {
59
+ const text = '@[550e8400-E29B-41d4-A716-446655440000] mixed case';
60
+ const result = extractMentions(text);
61
+ expect(result).toHaveLength(1);
62
+ });
63
+ });
64
+
65
+ describe('edge cases', () => {
66
+ it('should not extract malformed UUIDs', () => {
67
+ // Missing segment
68
+ expect(extractMentions('@[550e8400-e29b-41d4-a716] incomplete')).toEqual(
69
+ []
70
+ );
71
+
72
+ // Too short
73
+ expect(extractMentions('@[550e8400] too short')).toEqual([]);
74
+
75
+ // Invalid characters
76
+ expect(
77
+ extractMentions('@[550e8400-e29b-41d4-a716-44665544000g] invalid char')
78
+ ).toEqual([]);
79
+ });
80
+
81
+ it('should not extract @ without brackets', () => {
82
+ expect(extractMentions('@username plain mention')).toEqual([]);
83
+ expect(extractMentions('email@example.com')).toEqual([]);
84
+ });
85
+
86
+ it('should not extract incomplete bracket syntax', () => {
87
+ expect(
88
+ extractMentions(
89
+ '@[550e8400-e29b-41d4-a716-446655440000 missing bracket'
90
+ )
91
+ ).toEqual([]);
92
+ expect(
93
+ extractMentions(
94
+ '@550e8400-e29b-41d4-a716-446655440000] missing bracket'
95
+ )
96
+ ).toEqual([]);
97
+ });
98
+
99
+ it('should handle mentions at start of text', () => {
100
+ const text = '@[550e8400-e29b-41d4-a716-446655440000] at the start';
101
+ expect(extractMentions(text)).toEqual([
102
+ '550e8400-e29b-41d4-a716-446655440000',
103
+ ]);
104
+ });
105
+
106
+ it('should handle mentions at end of text', () => {
107
+ const text = 'At the end @[550e8400-e29b-41d4-a716-446655440000]';
108
+ expect(extractMentions(text)).toEqual([
109
+ '550e8400-e29b-41d4-a716-446655440000',
110
+ ]);
111
+ });
112
+
113
+ it('should handle mentions with no surrounding space', () => {
114
+ const text = 'text@[550e8400-e29b-41d4-a716-446655440000]more';
115
+ expect(extractMentions(text)).toEqual([
116
+ '550e8400-e29b-41d4-a716-446655440000',
117
+ ]);
118
+ });
119
+
120
+ it('should handle consecutive mentions', () => {
121
+ const text =
122
+ '@[550e8400-e29b-41d4-a716-446655440000]@[6ba7b810-9dad-11d1-80b4-00c04fd430c8]';
123
+ const result = extractMentions(text);
124
+ expect(result).toHaveLength(2);
125
+ });
126
+
127
+ it('should handle mentions in multiline text', () => {
128
+ const text = `
129
+ Line 1: @[550e8400-e29b-41d4-a716-446655440000]
130
+ Line 2: some text
131
+ Line 3: @[6ba7b810-9dad-11d1-80b4-00c04fd430c8]
132
+ `;
133
+ const result = extractMentions(text);
134
+ expect(result).toHaveLength(2);
135
+ });
136
+
137
+ it('should handle HTML content with mentions', () => {
138
+ const text =
139
+ '<p>Hello @[550e8400-e29b-41d4-a716-446655440000]</p><br>Test';
140
+ expect(extractMentions(text)).toEqual([
141
+ '550e8400-e29b-41d4-a716-446655440000',
142
+ ]);
143
+ });
144
+
145
+ it('should handle JSON-like content with mentions', () => {
146
+ const text =
147
+ '{"message": "Hello @[550e8400-e29b-41d4-a716-446655440000]"}';
148
+ expect(extractMentions(text)).toEqual([
149
+ '550e8400-e29b-41d4-a716-446655440000',
150
+ ]);
151
+ });
152
+ });
153
+
154
+ describe('real-world scenarios', () => {
155
+ it('should handle task description with mentions', () => {
156
+ const text = `
157
+ Please review this PR:
158
+ - @[550e8400-e29b-41d4-a716-446655440000] for frontend changes
159
+ - @[6ba7b810-9dad-11d1-80b4-00c04fd430c8] for backend changes
160
+
161
+ CC: @[f47ac10b-58cc-4372-a567-0e02b2c3d479]
162
+ `;
163
+ const result = extractMentions(text);
164
+ expect(result).toHaveLength(3);
165
+ });
166
+
167
+ it('should handle comment with single mention', () => {
168
+ const text =
169
+ '@[550e8400-e29b-41d4-a716-446655440000] can you take a look at this?';
170
+ expect(extractMentions(text)).toEqual([
171
+ '550e8400-e29b-41d4-a716-446655440000',
172
+ ]);
173
+ });
174
+
175
+ it('should handle markdown-style content', () => {
176
+ const text = `
177
+ # Task Title
178
+
179
+ Assigned to: @[550e8400-e29b-41d4-a716-446655440000]
180
+
181
+ ## Description
182
+ - [x] Complete task
183
+ - [ ] Review with @[6ba7b810-9dad-11d1-80b4-00c04fd430c8]
184
+ `;
185
+ const result = extractMentions(text);
186
+ expect(result).toHaveLength(2);
187
+ });
188
+ });
189
+ });
190
+
191
+ describe('NotificationType', () => {
192
+ it('should have expected notification types', () => {
193
+ // Type checking test - these should compile without errors
194
+ const types = [
195
+ 'task_assigned',
196
+ 'task_updated',
197
+ 'task_mention',
198
+ 'workspace_invite',
199
+ ] as const;
200
+
201
+ expect(types).toHaveLength(4);
202
+ });
203
+ });
204
+
205
+ describe('NotificationChannel', () => {
206
+ it('should have expected notification channels', () => {
207
+ const channels = ['web', 'email', 'sms', 'push'] as const;
208
+ expect(channels).toHaveLength(4);
209
+ });
210
+ });
@@ -0,0 +1,331 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ // Re-define constants and types here to avoid importing from the module
4
+ // that has server-side dependencies (@tuturuuu/supabase/next/client)
5
+ const ONBOARDING_STEPS = {
6
+ WELCOME: 'welcome',
7
+ WORKSPACE_SETUP: 'workspace_setup',
8
+ PROFILE_COMPLETION: 'profile_completion',
9
+ FEATURE_TOUR: 'feature_tour',
10
+ DASHBOARD_REDIRECT: 'dashboard_redirect',
11
+ } as const;
12
+
13
+ type OnboardingStep = (typeof ONBOARDING_STEPS)[keyof typeof ONBOARDING_STEPS];
14
+
15
+ interface OnboardingProgress {
16
+ user_id: string;
17
+ completed_steps: string[];
18
+ current_step: string;
19
+ workspace_name?: string | null;
20
+ workspace_description?: string | null;
21
+ workspace_avatar_url?: string | null;
22
+ profile_completed: boolean;
23
+ tour_completed: boolean;
24
+ completed_at?: string | null;
25
+ created_at: string;
26
+ updated_at: string;
27
+ }
28
+
29
+ interface WhitelistStatus {
30
+ is_whitelisted: boolean;
31
+ enabled: boolean;
32
+ allow_challenge_management: boolean;
33
+ allow_manage_all_challenges: boolean;
34
+ allow_role_management: boolean;
35
+ }
36
+
37
+ interface WorkspaceTemplate {
38
+ id: string;
39
+ name: string;
40
+ description?: string | null;
41
+ avatar_url?: string | null;
42
+ is_default: boolean;
43
+ created_at: string;
44
+ }
45
+
46
+ describe('ONBOARDING_STEPS', () => {
47
+ describe('step values', () => {
48
+ it('should have WELCOME step', () => {
49
+ expect(ONBOARDING_STEPS.WELCOME).toBe('welcome');
50
+ });
51
+
52
+ it('should have WORKSPACE_SETUP step', () => {
53
+ expect(ONBOARDING_STEPS.WORKSPACE_SETUP).toBe('workspace_setup');
54
+ });
55
+
56
+ it('should have PROFILE_COMPLETION step', () => {
57
+ expect(ONBOARDING_STEPS.PROFILE_COMPLETION).toBe('profile_completion');
58
+ });
59
+
60
+ it('should have FEATURE_TOUR step', () => {
61
+ expect(ONBOARDING_STEPS.FEATURE_TOUR).toBe('feature_tour');
62
+ });
63
+
64
+ it('should have DASHBOARD_REDIRECT step', () => {
65
+ expect(ONBOARDING_STEPS.DASHBOARD_REDIRECT).toBe('dashboard_redirect');
66
+ });
67
+ });
68
+
69
+ describe('step count', () => {
70
+ it('should have exactly 5 steps', () => {
71
+ const steps = Object.values(ONBOARDING_STEPS);
72
+ expect(steps).toHaveLength(5);
73
+ });
74
+
75
+ it('should have all unique values', () => {
76
+ const steps = Object.values(ONBOARDING_STEPS);
77
+ const uniqueSteps = new Set(steps);
78
+ expect(uniqueSteps.size).toBe(steps.length);
79
+ });
80
+ });
81
+
82
+ describe('step keys', () => {
83
+ it('should have expected keys', () => {
84
+ const keys = Object.keys(ONBOARDING_STEPS);
85
+ expect(keys).toContain('WELCOME');
86
+ expect(keys).toContain('WORKSPACE_SETUP');
87
+ expect(keys).toContain('PROFILE_COMPLETION');
88
+ expect(keys).toContain('FEATURE_TOUR');
89
+ expect(keys).toContain('DASHBOARD_REDIRECT');
90
+ });
91
+ });
92
+
93
+ describe('step order logic', () => {
94
+ it('welcome should be the first step (logically)', () => {
95
+ expect(ONBOARDING_STEPS.WELCOME).toBe('welcome');
96
+ });
97
+
98
+ it('dashboard_redirect should be the last step (logically)', () => {
99
+ expect(ONBOARDING_STEPS.DASHBOARD_REDIRECT).toBe('dashboard_redirect');
100
+ });
101
+ });
102
+ });
103
+
104
+ describe('OnboardingStep type', () => {
105
+ it('should accept valid step values', () => {
106
+ // Type checking tests - these should compile
107
+ const step1: OnboardingStep = 'welcome';
108
+ const step2: OnboardingStep = 'workspace_setup';
109
+ const step3: OnboardingStep = 'profile_completion';
110
+ const step4: OnboardingStep = 'feature_tour';
111
+ const step5: OnboardingStep = 'dashboard_redirect';
112
+
113
+ expect(step1).toBe('welcome');
114
+ expect(step2).toBe('workspace_setup');
115
+ expect(step3).toBe('profile_completion');
116
+ expect(step4).toBe('feature_tour');
117
+ expect(step5).toBe('dashboard_redirect');
118
+ });
119
+
120
+ it('should work with ONBOARDING_STEPS values', () => {
121
+ const step: OnboardingStep = ONBOARDING_STEPS.WELCOME;
122
+ expect(step).toBe('welcome');
123
+ });
124
+ });
125
+
126
+ describe('OnboardingProgress interface', () => {
127
+ it('should accept valid progress object', () => {
128
+ const progress: OnboardingProgress = {
129
+ user_id: 'user-123',
130
+ completed_steps: ['welcome', 'workspace_setup'],
131
+ current_step: 'profile_completion',
132
+ workspace_name: 'My Workspace',
133
+ workspace_description: 'A test workspace',
134
+ workspace_avatar_url: 'https://example.com/avatar.png',
135
+ profile_completed: true,
136
+ tour_completed: false,
137
+ completed_at: null,
138
+ created_at: '2024-01-01T00:00:00Z',
139
+ updated_at: '2024-01-02T00:00:00Z',
140
+ };
141
+
142
+ expect(progress.user_id).toBe('user-123');
143
+ expect(progress.completed_steps).toHaveLength(2);
144
+ expect(progress.current_step).toBe('profile_completion');
145
+ });
146
+
147
+ it('should accept progress with null optional fields', () => {
148
+ const progress: OnboardingProgress = {
149
+ user_id: 'user-123',
150
+ completed_steps: [],
151
+ current_step: 'welcome',
152
+ workspace_name: null,
153
+ workspace_description: null,
154
+ workspace_avatar_url: null,
155
+ profile_completed: false,
156
+ tour_completed: false,
157
+ completed_at: null,
158
+ created_at: '2024-01-01T00:00:00Z',
159
+ updated_at: '2024-01-01T00:00:00Z',
160
+ };
161
+
162
+ expect(progress.workspace_name).toBeNull();
163
+ expect(progress.completed_at).toBeNull();
164
+ });
165
+
166
+ it('should track completion with completed_at timestamp', () => {
167
+ const progress: OnboardingProgress = {
168
+ user_id: 'user-123',
169
+ completed_steps: Object.values(ONBOARDING_STEPS),
170
+ current_step: 'dashboard_redirect',
171
+ profile_completed: true,
172
+ tour_completed: true,
173
+ completed_at: '2024-01-15T12:00:00Z',
174
+ created_at: '2024-01-01T00:00:00Z',
175
+ updated_at: '2024-01-15T12:00:00Z',
176
+ };
177
+
178
+ expect(progress.completed_at).toBe('2024-01-15T12:00:00Z');
179
+ expect(progress.completed_steps).toHaveLength(5);
180
+ });
181
+ });
182
+
183
+ describe('WhitelistStatus interface', () => {
184
+ it('should accept valid whitelist status', () => {
185
+ const status: WhitelistStatus = {
186
+ is_whitelisted: true,
187
+ enabled: true,
188
+ allow_challenge_management: true,
189
+ allow_manage_all_challenges: false,
190
+ allow_role_management: true,
191
+ };
192
+
193
+ expect(status.is_whitelisted).toBe(true);
194
+ expect(status.enabled).toBe(true);
195
+ });
196
+
197
+ it('should represent non-whitelisted user', () => {
198
+ const status: WhitelistStatus = {
199
+ is_whitelisted: false,
200
+ enabled: false,
201
+ allow_challenge_management: false,
202
+ allow_manage_all_challenges: false,
203
+ allow_role_management: false,
204
+ };
205
+
206
+ expect(status.is_whitelisted).toBe(false);
207
+ expect(status.allow_challenge_management).toBe(false);
208
+ });
209
+
210
+ it('should represent partial permissions', () => {
211
+ const status: WhitelistStatus = {
212
+ is_whitelisted: true,
213
+ enabled: true,
214
+ allow_challenge_management: true,
215
+ allow_manage_all_challenges: false,
216
+ allow_role_management: false,
217
+ };
218
+
219
+ expect(status.is_whitelisted).toBe(true);
220
+ expect(status.allow_challenge_management).toBe(true);
221
+ expect(status.allow_manage_all_challenges).toBe(false);
222
+ });
223
+ });
224
+
225
+ describe('WorkspaceTemplate interface', () => {
226
+ it('should accept valid workspace template', () => {
227
+ const template: WorkspaceTemplate = {
228
+ id: 'template-123',
229
+ name: 'Business Template',
230
+ description: 'A template for business workspaces',
231
+ avatar_url: 'https://example.com/template.png',
232
+ is_default: true,
233
+ created_at: '2024-01-01T00:00:00Z',
234
+ };
235
+
236
+ expect(template.id).toBe('template-123');
237
+ expect(template.name).toBe('Business Template');
238
+ expect(template.is_default).toBe(true);
239
+ });
240
+
241
+ it('should accept template with null optional fields', () => {
242
+ const template: WorkspaceTemplate = {
243
+ id: 'template-456',
244
+ name: 'Minimal Template',
245
+ description: null,
246
+ avatar_url: null,
247
+ is_default: false,
248
+ created_at: '2024-01-01T00:00:00Z',
249
+ };
250
+
251
+ expect(template.description).toBeNull();
252
+ expect(template.avatar_url).toBeNull();
253
+ });
254
+
255
+ it('should handle default and non-default templates', () => {
256
+ const defaultTemplate: WorkspaceTemplate = {
257
+ id: 'default-1',
258
+ name: 'Default',
259
+ is_default: true,
260
+ created_at: '2024-01-01T00:00:00Z',
261
+ };
262
+
263
+ const customTemplate: WorkspaceTemplate = {
264
+ id: 'custom-1',
265
+ name: 'Custom',
266
+ is_default: false,
267
+ created_at: '2024-01-02T00:00:00Z',
268
+ };
269
+
270
+ expect(defaultTemplate.is_default).toBe(true);
271
+ expect(customTemplate.is_default).toBe(false);
272
+ });
273
+ });
274
+
275
+ describe('Onboarding flow logic', () => {
276
+ describe('step completion tracking', () => {
277
+ it('should be able to track incremental step completion', () => {
278
+ const completedSteps: OnboardingStep[] = [];
279
+
280
+ // Simulate completing steps
281
+ completedSteps.push(ONBOARDING_STEPS.WELCOME);
282
+ expect(completedSteps).toContain('welcome');
283
+
284
+ completedSteps.push(ONBOARDING_STEPS.WORKSPACE_SETUP);
285
+ expect(completedSteps).toHaveLength(2);
286
+
287
+ completedSteps.push(ONBOARDING_STEPS.PROFILE_COMPLETION);
288
+ completedSteps.push(ONBOARDING_STEPS.FEATURE_TOUR);
289
+ completedSteps.push(ONBOARDING_STEPS.DASHBOARD_REDIRECT);
290
+
291
+ expect(completedSteps).toHaveLength(5);
292
+ });
293
+
294
+ it('should be able to check if all steps are completed', () => {
295
+ const allSteps = Object.values(ONBOARDING_STEPS);
296
+ const completedSteps = [...allSteps];
297
+
298
+ const isCompleted = allSteps.every((step) =>
299
+ completedSteps.includes(step)
300
+ );
301
+ expect(isCompleted).toBe(true);
302
+ });
303
+
304
+ it('should detect incomplete onboarding', () => {
305
+ const allSteps = Object.values(ONBOARDING_STEPS);
306
+ const completedSteps: OnboardingStep[] = [
307
+ ONBOARDING_STEPS.WELCOME,
308
+ ONBOARDING_STEPS.WORKSPACE_SETUP,
309
+ ];
310
+
311
+ const isCompleted = allSteps.every((step) =>
312
+ completedSteps.includes(step)
313
+ );
314
+ expect(isCompleted).toBe(false);
315
+ });
316
+ });
317
+
318
+ describe('step deduplication', () => {
319
+ it('should handle duplicate step completion attempts', () => {
320
+ const completedSteps: OnboardingStep[] = [ONBOARDING_STEPS.WELCOME];
321
+
322
+ // Simulate adding same step again (should not duplicate)
323
+ const newStep = ONBOARDING_STEPS.WELCOME;
324
+ if (!completedSteps.includes(newStep)) {
325
+ completedSteps.push(newStep);
326
+ }
327
+
328
+ expect(completedSteps).toHaveLength(1);
329
+ });
330
+ });
331
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { joinPath, popPath } from '../path-helper';
3
+
4
+ describe('Path Helper', () => {
5
+ describe('joinPath', () => {
6
+ it('returns "/" for empty input', () => {
7
+ expect(joinPath()).toBe('/');
8
+ });
9
+
10
+ it('returns "/" for empty strings', () => {
11
+ expect(joinPath('', '')).toBe('/');
12
+ });
13
+
14
+ it('joins two simple paths without leading slash', () => {
15
+ // When first path doesn't start with /, no leading slash is added
16
+ expect(joinPath('folder', 'subfolder')).toBe('folder/subfolder');
17
+ });
18
+
19
+ it('joins multiple path segments', () => {
20
+ expect(joinPath('a', 'b', 'c', 'd')).toBe('a/b/c/d');
21
+ });
22
+
23
+ it('handles paths with slashes', () => {
24
+ expect(joinPath('/folder/', '/subfolder/')).toBe('/folder/subfolder/');
25
+ });
26
+
27
+ it('preserves leading slash for absolute paths', () => {
28
+ expect(joinPath('/root', 'child')).toBe('/root/child');
29
+ });
30
+
31
+ it('preserves relative path prefix ./', () => {
32
+ expect(joinPath('./relative', 'path')).toBe('./relative/path');
33
+ });
34
+
35
+ it('preserves trailing slash when present', () => {
36
+ expect(joinPath('folder', 'subfolder/')).toBe('folder/subfolder/');
37
+ });
38
+
39
+ it('handles mixed slashes with absolute first path', () => {
40
+ expect(joinPath('/folder/', '/subfolder')).toBe('/folder/subfolder');
41
+ });
42
+
43
+ it('filters out empty segments', () => {
44
+ expect(joinPath('folder', '', 'subfolder')).toBe('folder/subfolder');
45
+ });
46
+
47
+ it('skips dot segments', () => {
48
+ expect(joinPath('folder', '.', 'subfolder')).toBe('folder/subfolder');
49
+ });
50
+
51
+ it('handles whitespace in paths', () => {
52
+ expect(joinPath(' folder ', ' subfolder ')).toBe('folder/subfolder');
53
+ });
54
+
55
+ it('handles multiple consecutive slashes in absolute path', () => {
56
+ expect(joinPath('/folder///', '///subfolder')).toBe('/folder/subfolder');
57
+ });
58
+
59
+ it('handles double slash protocol prefix', () => {
60
+ expect(joinPath('//server', 'share')).toBe('/server/share');
61
+ });
62
+
63
+ it('handles single segment', () => {
64
+ expect(joinPath('single')).toBe('single');
65
+ });
66
+
67
+ it('handles single absolute segment', () => {
68
+ expect(joinPath('/single')).toBe('/single');
69
+ });
70
+
71
+ it('handles path with numbers', () => {
72
+ expect(joinPath('2024', '01', '15')).toBe('2024/01/15');
73
+ });
74
+
75
+ it('handles absolute path with numbers', () => {
76
+ expect(joinPath('/2024', '01', '15')).toBe('/2024/01/15');
77
+ });
78
+
79
+ it('handles paths with hyphens and underscores', () => {
80
+ expect(joinPath('my-folder', 'sub_folder')).toBe('my-folder/sub_folder');
81
+ });
82
+
83
+ it('handles absolute path with special chars', () => {
84
+ expect(joinPath('/my-folder', 'sub_folder')).toBe(
85
+ '/my-folder/sub_folder'
86
+ );
87
+ });
88
+ });
89
+
90
+ describe('popPath', () => {
91
+ it('returns "/" for empty input', () => {
92
+ expect(popPath('')).toBe('/');
93
+ });
94
+
95
+ it('returns "/" for root path', () => {
96
+ expect(popPath('/')).toBe('/');
97
+ });
98
+
99
+ it('returns "/" for single dot', () => {
100
+ expect(popPath('.')).toBe('/');
101
+ });
102
+
103
+ it('returns parent directory for simple path', () => {
104
+ expect(popPath('/folder/subfolder')).toBe('/folder');
105
+ });
106
+
107
+ it('returns "/" for single segment absolute path', () => {
108
+ expect(popPath('/folder')).toBe('/');
109
+ });
110
+
111
+ it('handles paths with trailing slashes', () => {
112
+ expect(popPath('/folder/subfolder/')).toBe('/folder');
113
+ });
114
+
115
+ it('handles paths with multiple trailing slashes', () => {
116
+ expect(popPath('/folder/subfolder///')).toBe('/folder');
117
+ });
118
+
119
+ it('handles relative paths starting with ./', () => {
120
+ expect(popPath('./folder/subfolder')).toBe('./folder');
121
+ });
122
+
123
+ it('returns "./" prefix for relative path with single segment', () => {
124
+ expect(popPath('./folder')).toBe('./.');
125
+ });
126
+
127
+ it('handles paths without leading slash', () => {
128
+ expect(popPath('folder/subfolder')).toBe('/folder');
129
+ });
130
+
131
+ it('handles deep paths', () => {
132
+ expect(popPath('/a/b/c/d/e')).toBe('/a/b/c/d');
133
+ });
134
+
135
+ it('handles paths with consecutive slashes', () => {
136
+ expect(popPath('/folder//subfolder')).toBe('/folder');
137
+ });
138
+
139
+ it('handles paths with numbers', () => {
140
+ expect(popPath('/2024/01/15')).toBe('/2024/01');
141
+ });
142
+
143
+ it('handles whitespace in paths', () => {
144
+ expect(popPath(' /folder/subfolder ')).toBe('/folder');
145
+ });
146
+
147
+ it('handles single segment without slash', () => {
148
+ // Function returns '/base' for non-relative single segment without slash
149
+ expect(popPath('folder')).toBe('/base');
150
+ });
151
+ });
152
+ });