@tuturuuu/utils 0.0.3 → 0.6.1

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 +313 -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,270 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveAppUrl, resolveInternalAppUrl } from '../app-url';
3
+ import {
4
+ getAppDomainByUrl,
5
+ getLocalInternalAppUrl,
6
+ getPortlessInternalAppUrl,
7
+ } from '../internal-domains';
8
+ import {
9
+ getTuturuuuPortlessAppOrigin,
10
+ TUTURUUU_PORTLESS_ALLOWED_DEV_ORIGINS,
11
+ TUTURUUU_PORTLESS_APP_ORIGINS,
12
+ } from '../portless';
13
+
14
+ describe('resolveAppUrl', () => {
15
+ it('uses the first valid configured URL', () => {
16
+ expect(
17
+ resolveAppUrl({
18
+ candidates: ['development', 'https://learn.tuturuuu.com/'],
19
+ fallback: 'http://localhost:7812',
20
+ })
21
+ ).toBe('https://learn.tuturuuu.com');
22
+ });
23
+
24
+ it('falls back when configured values are not absolute HTTP URLs', () => {
25
+ expect(
26
+ resolveAppUrl({
27
+ candidates: ['development', '', 'ftp://learn.tuturuuu.com'],
28
+ fallback: 'http://localhost:7812',
29
+ })
30
+ ).toBe('http://localhost:7812');
31
+ });
32
+
33
+ it('ignores wildcard listener URLs that are not browser destinations', () => {
34
+ expect(
35
+ resolveAppUrl({
36
+ candidates: ['http://0.0.0.0:7814', 'http://[::]:7814'],
37
+ fallback: 'https://hive.tuturuuu.com',
38
+ })
39
+ ).toBe('https://hive.tuturuuu.com');
40
+ });
41
+ });
42
+
43
+ describe('resolveInternalAppUrl', () => {
44
+ it('skips configured URLs that belong to another registered internal app', () => {
45
+ expect(
46
+ resolveInternalAppUrl({
47
+ appName: 'learn',
48
+ candidates: ['https://tuturuuu.com', 'https://learn.tuturuuu.com'],
49
+ fallback: 'http://localhost:7812',
50
+ })
51
+ ).toBe('https://learn.tuturuuu.com');
52
+ });
53
+
54
+ it('recognizes Portless local origins as registered internal app URLs', () => {
55
+ expect(
56
+ resolveInternalAppUrl({
57
+ appName: 'learn',
58
+ candidates: [
59
+ 'https://tuturuuu.localhost',
60
+ 'https://learn.tuturuuu.localhost',
61
+ ],
62
+ fallback: 'http://localhost:7812',
63
+ })
64
+ ).toBe('https://learn.tuturuuu.localhost');
65
+ });
66
+
67
+ it('recognizes worktree-prefixed Portless origins as registered app URLs', () => {
68
+ expect(
69
+ resolveInternalAppUrl({
70
+ appName: 'chat',
71
+ candidates: ['https://zalo-qr-chat-setup.chat.tuturuuu.localhost'],
72
+ fallback: 'http://localhost:7821',
73
+ })
74
+ ).toBe('https://zalo-qr-chat-setup.chat.tuturuuu.localhost');
75
+ expect(
76
+ resolveInternalAppUrl({
77
+ appName: 'platform',
78
+ candidates: [
79
+ 'https://zalo-qr-chat-setup.chat.tuturuuu.localhost',
80
+ 'https://zalo-qr-chat-setup.tuturuuu.localhost',
81
+ ],
82
+ fallback: 'http://localhost:7803',
83
+ })
84
+ ).toBe('https://zalo-qr-chat-setup.tuturuuu.localhost');
85
+ expect(
86
+ getAppDomainByUrl(
87
+ 'https://zalo-qr-chat-setup.chat.tuturuuu.localhost/verify-token'
88
+ )?.name
89
+ ).toBe('chat');
90
+ });
91
+
92
+ it('recognizes local Portless proxy ports for registered app URLs', () => {
93
+ expect(
94
+ getAppDomainByUrl(
95
+ 'https://tuturuuu.localhost:1355/verify-token?nextUrl=%2Fen%2Fpersonal%2Ftasks'
96
+ )
97
+ ).toMatchObject({
98
+ canonicalUrl:
99
+ 'https://tuturuuu.localhost/verify-token?nextUrl=%2Fen%2Fpersonal%2Ftasks',
100
+ kind: 'internal',
101
+ name: 'platform',
102
+ });
103
+ expect(
104
+ getAppDomainByUrl(
105
+ 'https://tasks.tuturuuu.localhost:1355/verify-token?nextUrl=%2Fpersonal'
106
+ )
107
+ ).toMatchObject({
108
+ canonicalUrl:
109
+ 'https://tasks.tuturuuu.localhost/verify-token?nextUrl=%2Fpersonal',
110
+ kind: 'internal',
111
+ name: 'tasks',
112
+ });
113
+ expect(
114
+ getAppDomainByUrl(
115
+ 'https://tuturuuu.localhost.evil.test:1355/verify-token?nextUrl=%2F'
116
+ )
117
+ ).toBeNull();
118
+ });
119
+
120
+ it('canonicalizes production internal app URLs to their registered HTTPS origins', () => {
121
+ expect(
122
+ resolveInternalAppUrl({
123
+ appName: 'platform',
124
+ candidates: ['http://tuturuuu.com/'],
125
+ fallback: 'http://localhost:7803',
126
+ })
127
+ ).toBe('https://tuturuuu.com');
128
+ expect(
129
+ resolveInternalAppUrl({
130
+ appName: 'meet',
131
+ candidates: ['http://meet.tuturuuu.com/'],
132
+ fallback: 'http://localhost:7807',
133
+ })
134
+ ).toBe('https://meet.tuturuuu.com');
135
+ });
136
+
137
+ it('falls back when every configured URL points at another internal app', () => {
138
+ expect(
139
+ resolveInternalAppUrl({
140
+ appName: 'nova',
141
+ candidates: ['https://tuturuuu.com', 'https://learn.tuturuuu.com'],
142
+ fallback: 'https://nova.ai.vn',
143
+ })
144
+ ).toBe('https://nova.ai.vn');
145
+ });
146
+
147
+ it('keeps custom app origins that are not registered to another app', () => {
148
+ expect(
149
+ resolveInternalAppUrl({
150
+ appName: 'cms',
151
+ candidates: ['https://cms-preview.example.com/'],
152
+ fallback: 'https://cms.tuturuuu.com',
153
+ })
154
+ ).toBe('https://cms-preview.example.com');
155
+ });
156
+
157
+ it('skips wildcard listener origins before resolving an internal app URL', () => {
158
+ expect(
159
+ resolveInternalAppUrl({
160
+ appName: 'hive',
161
+ candidates: ['http://0.0.0.0:7814', 'https://hive.tuturuuu.com'],
162
+ fallback: 'http://localhost:7814',
163
+ })
164
+ ).toBe('https://hive.tuturuuu.com');
165
+ });
166
+ });
167
+
168
+ describe('getLocalInternalAppUrl', () => {
169
+ it('prefers the Portless Tuturuuu domain for local internal app defaults', () => {
170
+ expect(getLocalInternalAppUrl('platform', 'http://localhost:7803')).toBe(
171
+ 'https://tuturuuu.localhost'
172
+ );
173
+ expect(getLocalInternalAppUrl('tasks', 'http://localhost:7809')).toBe(
174
+ 'https://tasks.tuturuuu.localhost'
175
+ );
176
+ });
177
+
178
+ it.each([
179
+ ['apps', 'https://apps.tuturuuu.localhost'],
180
+ ['platform', 'https://tuturuuu.localhost'],
181
+ ['cms', 'https://cms.tuturuuu.localhost'],
182
+ ['calendar', 'https://calendar.tuturuuu.localhost'],
183
+ ['chat', 'https://chat.tuturuuu.localhost'],
184
+ ['drive', 'https://drive.tuturuuu.localhost'],
185
+ ['qr', 'https://qr.tuturuuu.localhost'],
186
+ ['nova', 'https://nova.tuturuuu.localhost'],
187
+ ['rewise', 'https://rewise.tuturuuu.localhost'],
188
+ ['tasks', 'https://tasks.tuturuuu.localhost'],
189
+ ['finance', 'https://finance.tuturuuu.localhost'],
190
+ ['inventory', 'https://inventory.tuturuuu.localhost'],
191
+ ['track', 'https://track.tuturuuu.localhost'],
192
+ ['learn', 'https://learn.tuturuuu.localhost'],
193
+ ['mail', 'https://mail.tuturuuu.localhost'],
194
+ ['meet', 'https://meet.tuturuuu.localhost'],
195
+ ['teach', 'https://teach.tuturuuu.localhost'],
196
+ ['hive', 'https://hive.tuturuuu.localhost'],
197
+ ['mind', 'https://mind.tuturuuu.localhost'],
198
+ ] as const)('returns the %s Portless URL', (appName, expectedUrl) => {
199
+ expect(getPortlessInternalAppUrl(appName)).toBe(expectedUrl);
200
+ expect(getLocalInternalAppUrl(appName, 'http://localhost:9999')).toBe(
201
+ expectedUrl
202
+ );
203
+ });
204
+
205
+ it('uses the current Portless worktree prefix for local peer app defaults', () => {
206
+ const originalPortlessUrl = process.env.PORTLESS_URL;
207
+
208
+ try {
209
+ process.env.PORTLESS_URL =
210
+ 'https://zalo-qr-chat-setup.chat.tuturuuu.localhost';
211
+
212
+ expect(getTuturuuuPortlessAppOrigin('platform')).toBe(
213
+ 'https://zalo-qr-chat-setup.tuturuuu.localhost'
214
+ );
215
+ expect(getPortlessInternalAppUrl('chat')).toBe(
216
+ 'https://zalo-qr-chat-setup.chat.tuturuuu.localhost'
217
+ );
218
+ expect(getLocalInternalAppUrl('tasks', 'http://localhost:7809')).toBe(
219
+ 'https://zalo-qr-chat-setup.tasks.tuturuuu.localhost'
220
+ );
221
+ } finally {
222
+ if (originalPortlessUrl === undefined) {
223
+ delete process.env.PORTLESS_URL;
224
+ } else {
225
+ process.env.PORTLESS_URL = originalPortlessUrl;
226
+ }
227
+ }
228
+ });
229
+ });
230
+
231
+ describe('Portless app origin registry', () => {
232
+ it('keeps all app origins under the Tuturuuu localhost namespace', () => {
233
+ expect(TUTURUUU_PORTLESS_ALLOWED_DEV_ORIGINS).toEqual([
234
+ 'tuturuuu.localhost',
235
+ '*.tuturuuu.localhost',
236
+ ]);
237
+ expect(Object.values(TUTURUUU_PORTLESS_APP_ORIGINS)).toEqual([
238
+ 'https://apps.tuturuuu.localhost',
239
+ 'https://calendar.tuturuuu.localhost',
240
+ 'https://chat.tuturuuu.localhost',
241
+ 'https://cms.tuturuuu.localhost',
242
+ 'https://drive.tuturuuu.localhost',
243
+ 'https://external.tuturuuu.localhost',
244
+ 'https://finance.tuturuuu.localhost',
245
+ 'https://hive.tuturuuu.localhost',
246
+ 'https://realtime.hive.tuturuuu.localhost',
247
+ 'https://inventory.tuturuuu.localhost',
248
+ 'https://learn.tuturuuu.localhost',
249
+ 'https://mail.tuturuuu.localhost',
250
+ 'https://meet.tuturuuu.localhost',
251
+ 'https://mind.tuturuuu.localhost',
252
+ 'https://nova.tuturuuu.localhost',
253
+ 'https://tuturuuu.localhost',
254
+ 'https://playground.tuturuuu.localhost',
255
+ 'https://qr.tuturuuu.localhost',
256
+ 'https://rewise.tuturuuu.localhost',
257
+ 'https://shortener.tuturuuu.localhost',
258
+ 'https://storefront.tuturuuu.localhost',
259
+ 'https://tasks.tuturuuu.localhost',
260
+ 'https://teach.tuturuuu.localhost',
261
+ 'https://track.tuturuuu.localhost',
262
+ ]);
263
+ });
264
+
265
+ it('does not reuse the same Portless origin for multiple apps', () => {
266
+ const origins = Object.values(TUTURUUU_PORTLESS_APP_ORIGINS);
267
+
268
+ expect(new Set(origins).size).toBe(origins.length);
269
+ });
270
+ });
@@ -0,0 +1,97 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { normalizeAvatarImageSrc } from '../avatar-url';
3
+
4
+ const SUPABASE_PUBLIC_BARE_UUID_AVATAR_URL =
5
+ 'https://yjbjpmwbfimjcdsjxfst.supabase.co/storage/v1/object/public/avatars/bbaf2747-4452-4b56-910d-0b313f49843e';
6
+ const CURRENT_SUPABASE_URL = 'https://hvgmshmjolwfcbsxmyku.supabase.co';
7
+ const ORIGINAL_SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
8
+
9
+ beforeEach(() => {
10
+ delete process.env.NEXT_PUBLIC_SUPABASE_URL;
11
+ });
12
+
13
+ afterEach(() => {
14
+ if (ORIGINAL_SUPABASE_URL === undefined) {
15
+ delete process.env.NEXT_PUBLIC_SUPABASE_URL;
16
+ } else {
17
+ process.env.NEXT_PUBLIC_SUPABASE_URL = ORIGINAL_SUPABASE_URL;
18
+ }
19
+ });
20
+
21
+ describe('normalizeAvatarImageSrc', () => {
22
+ it('drops blank and unsafe avatar values', () => {
23
+ expect(normalizeAvatarImageSrc(undefined)).toBeUndefined();
24
+ expect(normalizeAvatarImageSrc(null)).toBeUndefined();
25
+ expect(normalizeAvatarImageSrc('')).toBeUndefined();
26
+ expect(normalizeAvatarImageSrc(' ')).toBeUndefined();
27
+ expect(normalizeAvatarImageSrc('//example.com/avatar.png')).toBeUndefined();
28
+ });
29
+
30
+ it('drops bare UUID avatar object identifiers', () => {
31
+ expect(
32
+ normalizeAvatarImageSrc('bbaf2747-4452-4b56-910d-0b313f49843e')
33
+ ).toBeUndefined();
34
+ });
35
+
36
+ it('keeps Supabase public avatar URLs even when the object key is a UUID', () => {
37
+ expect(normalizeAvatarImageSrc(SUPABASE_PUBLIC_BARE_UUID_AVATAR_URL)).toBe(
38
+ SUPABASE_PUBLIC_BARE_UUID_AVATAR_URL
39
+ );
40
+ });
41
+
42
+ it('keeps Supabase public avatar URLs on their original project', () => {
43
+ process.env.NEXT_PUBLIC_SUPABASE_URL = CURRENT_SUPABASE_URL;
44
+
45
+ expect(normalizeAvatarImageSrc(SUPABASE_PUBLIC_BARE_UUID_AVATAR_URL)).toBe(
46
+ SUPABASE_PUBLIC_BARE_UUID_AVATAR_URL
47
+ );
48
+ });
49
+
50
+ it('repairs malformed Supabase public avatar object URLs', () => {
51
+ process.env.NEXT_PUBLIC_SUPABASE_URL = CURRENT_SUPABASE_URL;
52
+
53
+ expect(
54
+ normalizeAvatarImageSrc(
55
+ 'https://fnzamlzqfdwaaxdefwraj.supabase.co/storage/v1/object/v1/public/avatars/bbaf2747-4452-4b56-910d-0b313f49843e'
56
+ )
57
+ ).toBe(
58
+ 'https://fnzamlzqfdwaaxdefwraj.supabase.co/storage/v1/object/public/avatars/bbaf2747-4452-4b56-910d-0b313f49843e'
59
+ );
60
+ });
61
+
62
+ it('repairs malformed Supabase avatar paths without project env config', () => {
63
+ expect(
64
+ normalizeAvatarImageSrc(
65
+ 'https://fnzamlzqfdwaaxdefwraj.supabase.co/storage/v1/object/v1/public/avatars/bbaf2747-4452-4b56-910d-0b313f49843e'
66
+ )
67
+ ).toBe(
68
+ 'https://fnzamlzqfdwaaxdefwraj.supabase.co/storage/v1/object/public/avatars/bbaf2747-4452-4b56-910d-0b313f49843e'
69
+ );
70
+ });
71
+
72
+ it('keeps supported image source schemes and paths', () => {
73
+ expect(normalizeAvatarImageSrc('https://example.com/avatar.png')).toBe(
74
+ 'https://example.com/avatar.png'
75
+ );
76
+ expect(normalizeAvatarImageSrc('/avatars/local.png')).toBe(
77
+ '/avatars/local.png'
78
+ );
79
+ expect(normalizeAvatarImageSrc('blob:avatar.png')).toBe('blob:avatar.png');
80
+ expect(normalizeAvatarImageSrc('data:image/png;base64,abc')).toBe(
81
+ 'data:image/png;base64,abc'
82
+ );
83
+ });
84
+
85
+ it('keeps real Supabase avatar object paths', () => {
86
+ expect(
87
+ normalizeAvatarImageSrc('avatars/user-1/avatar-1700000000000.png')
88
+ ).toBe('avatars/user-1/avatar-1700000000000.png');
89
+ expect(
90
+ normalizeAvatarImageSrc(
91
+ 'https://hvgmshmjolwfcbsxmyku.supabase.co/storage/v1/object/public/avatars/00000000-0000-4000-8000-000000000001/1770694181366.jpg'
92
+ )
93
+ ).toBe(
94
+ 'https://hvgmshmjolwfcbsxmyku.supabase.co/storage/v1/object/public/avatars/00000000-0000-4000-8000-000000000001/1770694181366.jpg'
95
+ );
96
+ });
97
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Tests for Color Helper Utilities
3
+ *
4
+ * Tests the getEventStyles function which returns Tailwind CSS classes
5
+ * for calendar event styling based on color input.
6
+ */
7
+
8
+ import { describe, expect, it } from 'vitest';
9
+ import { getEventStyles } from '../color-helper';
10
+
11
+ describe('getEventStyles', () => {
12
+ describe('Known colors', () => {
13
+ it('should return correct styles for BLUE', () => {
14
+ const styles = getEventStyles('BLUE');
15
+ expect(styles.bg).toContain('bg-calendar-bg-blue');
16
+ expect(styles.border).toContain('border-dynamic-light-blue');
17
+ expect(styles.text).toContain('text-dynamic-light-blue');
18
+ expect(styles.dragBg).toBe('bg-calendar-bg-blue');
19
+ expect(styles.syncingBg).toBe('bg-calendar-bg-blue');
20
+ expect(styles.successBg).toBe('bg-calendar-bg-blue');
21
+ expect(styles.errorBg).toBe('bg-calendar-bg-red');
22
+ });
23
+
24
+ it('should return correct styles for RED', () => {
25
+ const styles = getEventStyles('RED');
26
+ expect(styles.bg).toContain('bg-calendar-bg-red');
27
+ expect(styles.border).toContain('border-dynamic-light-red');
28
+ expect(styles.text).toContain('text-dynamic-light-red');
29
+ });
30
+
31
+ it('should return correct styles for GREEN', () => {
32
+ const styles = getEventStyles('GREEN');
33
+ expect(styles.bg).toContain('bg-calendar-bg-green');
34
+ expect(styles.border).toContain('border-dynamic-light-green');
35
+ expect(styles.text).toContain('text-dynamic-light-green');
36
+ });
37
+
38
+ it('should return correct styles for YELLOW', () => {
39
+ const styles = getEventStyles('YELLOW');
40
+ expect(styles.bg).toContain('bg-calendar-bg-yellow');
41
+ expect(styles.border).toContain('border-dynamic-light-yellow');
42
+ expect(styles.text).toContain('text-dynamic-light-yellow');
43
+ });
44
+
45
+ it('should return correct styles for PURPLE', () => {
46
+ const styles = getEventStyles('PURPLE');
47
+ expect(styles.bg).toContain('bg-calendar-bg-purple');
48
+ expect(styles.border).toContain('border-dynamic-light-purple');
49
+ expect(styles.text).toContain('text-dynamic-light-purple');
50
+ });
51
+
52
+ it('should return correct styles for PINK', () => {
53
+ const styles = getEventStyles('PINK');
54
+ expect(styles.bg).toContain('bg-calendar-bg-pink');
55
+ expect(styles.border).toContain('border-dynamic-light-pink');
56
+ expect(styles.text).toContain('text-dynamic-light-pink');
57
+ });
58
+
59
+ it('should return correct styles for ORANGE', () => {
60
+ const styles = getEventStyles('ORANGE');
61
+ expect(styles.bg).toContain('bg-calendar-bg-orange');
62
+ expect(styles.border).toContain('border-dynamic-light-orange');
63
+ expect(styles.text).toContain('text-dynamic-light-orange');
64
+ });
65
+
66
+ it('should return correct styles for INDIGO', () => {
67
+ const styles = getEventStyles('INDIGO');
68
+ expect(styles.bg).toContain('bg-calendar-bg-indigo');
69
+ expect(styles.border).toContain('border-dynamic-light-indigo');
70
+ expect(styles.text).toContain('text-dynamic-light-indigo');
71
+ });
72
+
73
+ it('should return correct styles for CYAN', () => {
74
+ const styles = getEventStyles('CYAN');
75
+ expect(styles.bg).toContain('bg-calendar-bg-cyan');
76
+ expect(styles.border).toContain('border-dynamic-light-cyan');
77
+ expect(styles.text).toContain('text-dynamic-light-cyan');
78
+ });
79
+
80
+ it('should return correct styles for GRAY', () => {
81
+ const styles = getEventStyles('GRAY');
82
+ expect(styles.bg).toContain('bg-calendar-bg-gray');
83
+ expect(styles.border).toContain('border-dynamic-light-gray');
84
+ expect(styles.text).toContain('text-dynamic-light-gray');
85
+ });
86
+ });
87
+
88
+ describe('Case normalization', () => {
89
+ it('should handle lowercase color names', () => {
90
+ const styles = getEventStyles('blue');
91
+ expect(styles.bg).toContain('bg-calendar-bg-blue');
92
+ });
93
+
94
+ it('should handle mixed case color names', () => {
95
+ const styles = getEventStyles('Blue');
96
+ expect(styles.bg).toContain('bg-calendar-bg-blue');
97
+ });
98
+
99
+ it('should handle lowercase purple', () => {
100
+ const styles = getEventStyles('purple');
101
+ expect(styles.bg).toContain('bg-calendar-bg-purple');
102
+ });
103
+ });
104
+
105
+ describe('Invalid/unknown colors', () => {
106
+ it('should return BLUE styles for unknown color', () => {
107
+ const styles = getEventStyles('UNKNOWN');
108
+ expect(styles.bg).toContain('bg-calendar-bg-blue');
109
+ expect(styles.text).toContain('text-dynamic-light-blue');
110
+ });
111
+
112
+ it('should return BLUE styles for empty string', () => {
113
+ const styles = getEventStyles('');
114
+ expect(styles.bg).toContain('bg-calendar-bg-blue');
115
+ });
116
+
117
+ it('should return BLUE styles for null/undefined', () => {
118
+ const styles = getEventStyles(null as any);
119
+ expect(styles.bg).toContain('bg-calendar-bg-blue');
120
+
121
+ const styles2 = getEventStyles(undefined as any);
122
+ expect(styles2.bg).toContain('bg-calendar-bg-blue');
123
+ });
124
+ });
125
+
126
+ describe('Return structure', () => {
127
+ it('should return object with all required properties', () => {
128
+ const styles = getEventStyles('BLUE');
129
+ expect(styles).toHaveProperty('bg');
130
+ expect(styles).toHaveProperty('border');
131
+ expect(styles).toHaveProperty('text');
132
+ expect(styles).toHaveProperty('dragBg');
133
+ expect(styles).toHaveProperty('syncingBg');
134
+ expect(styles).toHaveProperty('successBg');
135
+ expect(styles).toHaveProperty('errorBg');
136
+ });
137
+
138
+ it('should return string values for all properties', () => {
139
+ const styles = getEventStyles('GREEN');
140
+ expect(typeof styles.bg).toBe('string');
141
+ expect(typeof styles.border).toBe('string');
142
+ expect(typeof styles.text).toBe('string');
143
+ expect(typeof styles.dragBg).toBe('string');
144
+ expect(typeof styles.syncingBg).toBe('string');
145
+ expect(typeof styles.successBg).toBe('string');
146
+ expect(typeof styles.errorBg).toBe('string');
147
+ });
148
+
149
+ it('should always have errorBg as red', () => {
150
+ const colors = [
151
+ 'BLUE',
152
+ 'GREEN',
153
+ 'YELLOW',
154
+ 'PURPLE',
155
+ 'PINK',
156
+ 'ORANGE',
157
+ 'INDIGO',
158
+ 'CYAN',
159
+ 'GRAY',
160
+ ];
161
+ for (const color of colors) {
162
+ const styles = getEventStyles(color);
163
+ expect(styles.errorBg).toBe('bg-calendar-bg-red');
164
+ }
165
+ });
166
+ });
167
+
168
+ describe('Hover styles', () => {
169
+ it('should include hover ring styles in bg property', () => {
170
+ const styles = getEventStyles('BLUE');
171
+ expect(styles.bg).toContain('hover:ring-dynamic-light-blue');
172
+ });
173
+
174
+ it('should have opacity modifier in border', () => {
175
+ const styles = getEventStyles('RED');
176
+ expect(styles.border).toContain('/80');
177
+ });
178
+ });
179
+ });