@lenne.tech/nest-server 11.32.4 → 11.33.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 (260) hide show
  1. package/.claude/rules/architecture.md +16 -0
  2. package/.claude/rules/configurable-features.md +11 -3
  3. package/.claude/rules/testing.md +101 -1
  4. package/CLAUDE.md +5 -0
  5. package/FRAMEWORK-API.md +58 -3
  6. package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
  7. package/dist/core/common/helpers/content-disposition.helper.js +14 -0
  8. package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
  9. package/dist/core/common/helpers/file.helper.d.ts +7 -0
  10. package/dist/core/common/helpers/file.helper.js +21 -4
  11. package/dist/core/common/helpers/file.helper.js.map +1 -1
  12. package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
  13. package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
  14. package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
  15. package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
  16. package/dist/core/common/helpers/project-name.helper.js +38 -0
  17. package/dist/core/common/helpers/project-name.helper.js.map +1 -0
  18. package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
  19. package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
  20. package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
  21. package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
  22. package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
  23. package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
  24. package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
  25. package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
  26. package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
  27. package/dist/core/common/services/core-cron-jobs.service.js +278 -27
  28. package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
  29. package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
  30. package/dist/core/common/services/core-redis-pubsub.js +112 -0
  31. package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
  32. package/dist/core/common/services/core-redis.service.d.ts +25 -0
  33. package/dist/core/common/services/core-redis.service.js +154 -0
  34. package/dist/core/common/services/core-redis.service.js.map +1 -0
  35. package/dist/core/common/services/core-s3.service.d.ts +51 -0
  36. package/dist/core/common/services/core-s3.service.js +261 -0
  37. package/dist/core/common/services/core-s3.service.js.map +1 -0
  38. package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
  39. package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
  40. package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
  41. package/dist/core/common/services/rate-limit-store.d.ts +61 -0
  42. package/dist/core/common/services/rate-limit-store.js +235 -0
  43. package/dist/core/common/services/rate-limit-store.js.map +1 -0
  44. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
  45. package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
  46. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  47. package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
  48. package/dist/core/modules/ai/services/core-ai.service.js +24 -20
  49. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  50. package/dist/core/modules/auth/core-auth.module.js +4 -1
  51. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  52. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
  53. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
  54. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
  55. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
  56. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
  57. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
  58. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
  59. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
  60. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  61. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
  62. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
  63. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  64. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
  65. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
  66. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  67. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
  68. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  69. package/dist/core/modules/file/core-file.controller.d.ts +11 -2
  70. package/dist/core/modules/file/core-file.controller.js +52 -15
  71. package/dist/core/modules/file/core-file.controller.js.map +1 -1
  72. package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
  73. package/dist/core/modules/file/core-file.resolver.js +23 -16
  74. package/dist/core/modules/file/core-file.resolver.js.map +1 -1
  75. package/dist/core/modules/file/core-file.service.d.ts +41 -7
  76. package/dist/core/modules/file/core-file.service.js +227 -7
  77. package/dist/core/modules/file/core-file.service.js.map +1 -1
  78. package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
  79. package/dist/core/modules/file/file-metadata.helper.js +33 -0
  80. package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
  81. package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
  82. package/dist/core/modules/file/file-roles.helper.js +53 -0
  83. package/dist/core/modules/file/file-roles.helper.js.map +1 -0
  84. package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
  85. package/dist/core/modules/file/file-storage.helper.js +65 -0
  86. package/dist/core/modules/file/file-storage.helper.js.map +1 -0
  87. package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
  88. package/dist/core/modules/file/filesystem-file.helper.js +77 -0
  89. package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
  90. package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
  91. package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
  92. package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
  93. package/dist/core/modules/file/s3-file.helper.js +81 -0
  94. package/dist/core/modules/file/s3-file.helper.js.map +1 -0
  95. package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
  96. package/dist/core/modules/hub/core-hub.controller.js +24 -22
  97. package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
  98. package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
  99. package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
  100. package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
  101. package/dist/core/modules/hub/hub-buffer.js +101 -0
  102. package/dist/core/modules/hub/hub-buffer.js.map +1 -0
  103. package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
  104. package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
  105. package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
  106. package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
  107. package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
  108. package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
  109. package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
  110. package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
  111. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
  112. package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
  113. package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
  114. package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
  115. package/dist/core/modules/migrate/helpers/migration.helper.d.ts +2 -6
  116. package/dist/core/modules/migrate/helpers/migration.helper.js +4 -4
  117. package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
  118. package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
  119. package/dist/core/modules/migrate/migration-runner.js +4 -0
  120. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  121. package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
  122. package/dist/core/modules/migrate/mongo-state-store.js +98 -9
  123. package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
  124. package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
  125. package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
  126. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  127. package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
  128. package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
  129. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  130. package/dist/core/modules/tenant/core-tenant.module.js +3 -4
  131. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  132. package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
  133. package/dist/core/modules/tus/core-tus.controller.js +29 -3
  134. package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
  135. package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
  136. package/dist/core/modules/tus/core-tus.service.js +182 -28
  137. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  138. package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
  139. package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
  140. package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
  141. package/dist/core/modules/tus/tus-redis-locker.js +87 -0
  142. package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
  143. package/dist/core/modules/tus/tus.module.d.ts +1 -0
  144. package/dist/core/modules/tus/tus.module.js +30 -4
  145. package/dist/core/modules/tus/tus.module.js.map +1 -1
  146. package/dist/core.module.js +18 -1
  147. package/dist/core.module.js.map +1 -1
  148. package/dist/index.d.ts +10 -0
  149. package/dist/index.js +10 -0
  150. package/dist/index.js.map +1 -1
  151. package/dist/main.js +2 -1
  152. package/dist/main.js.map +1 -1
  153. package/dist/server/modules/file/file.controller.js.map +1 -1
  154. package/dist/server/modules/file/file.resolver.js +1 -12
  155. package/dist/server/modules/file/file.resolver.js.map +1 -1
  156. package/dist/server/modules/file/file.service.d.ts +5 -1
  157. package/dist/server/modules/file/file.service.js +12 -3
  158. package/dist/server/modules/file/file.service.js.map +1 -1
  159. package/dist/server/modules/user/avatar.controller.d.ts +5 -1
  160. package/dist/server/modules/user/avatar.controller.js +26 -10
  161. package/dist/server/modules/user/avatar.controller.js.map +1 -1
  162. package/dist/server/modules/user/user.model.js +3 -2
  163. package/dist/server/modules/user/user.model.js.map +1 -1
  164. package/dist/server/modules/user/user.module.js +6 -2
  165. package/dist/server/modules/user/user.module.js.map +1 -1
  166. package/dist/server/modules/user/user.service.d.ts +1 -1
  167. package/dist/server/modules/user/user.service.js +6 -13
  168. package/dist/server/modules/user/user.service.js.map +1 -1
  169. package/dist/tsconfig.build.tsbuildinfo +1 -1
  170. package/docs/REQUEST-LIFECYCLE.md +57 -8
  171. package/docs/security-overrides.md +21 -3
  172. package/migration-guides/11.32.3-to-11.32.4.md +15 -3
  173. package/migration-guides/11.32.x-to-11.33.x.md +1551 -0
  174. package/package.json +74 -42
  175. package/src/core/common/helpers/content-disposition.helper.ts +90 -0
  176. package/src/core/common/helpers/file.helper.ts +53 -10
  177. package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
  178. package/src/core/common/helpers/project-name.helper.ts +71 -0
  179. package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
  180. package/src/core/common/interfaces/server-options.interface.ts +439 -2
  181. package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
  182. package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
  183. package/src/core/common/services/core-cron-jobs.service.ts +589 -47
  184. package/src/core/common/services/core-redis-pubsub.ts +183 -0
  185. package/src/core/common/services/core-redis.service.ts +249 -0
  186. package/src/core/common/services/core-s3.service.ts +465 -0
  187. package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
  188. package/src/core/common/services/rate-limit-store.ts +492 -0
  189. package/src/core/modules/ai/README.md +25 -0
  190. package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
  191. package/src/core/modules/ai/services/core-ai.service.ts +33 -24
  192. package/src/core/modules/auth/core-auth.module.ts +7 -1
  193. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
  194. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
  195. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
  196. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
  197. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
  198. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
  199. package/src/core/modules/file/INTEGRATION-CHECKLIST.md +108 -0
  200. package/src/core/modules/file/README.md +277 -53
  201. package/src/core/modules/file/core-file.controller.ts +168 -12
  202. package/src/core/modules/file/core-file.resolver.ts +46 -12
  203. package/src/core/modules/file/core-file.service.ts +482 -16
  204. package/src/core/modules/file/file-metadata.helper.ts +142 -0
  205. package/src/core/modules/file/file-roles.helper.ts +110 -0
  206. package/src/core/modules/file/file-storage.helper.ts +163 -0
  207. package/src/core/modules/file/filesystem-file.helper.ts +184 -0
  208. package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
  209. package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
  210. package/src/core/modules/file/s3-file.helper.ts +209 -0
  211. package/src/core/modules/hub/README.md +48 -0
  212. package/src/core/modules/hub/core-hub.controller.ts +18 -16
  213. package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
  214. package/src/core/modules/hub/hub-buffer.ts +198 -0
  215. package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
  216. package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
  217. package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
  218. package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
  219. package/src/core/modules/migrate/README.md +33 -6
  220. package/src/core/modules/migrate/helpers/migration.helper.ts +13 -6
  221. package/src/core/modules/migrate/migration-runner.ts +10 -1
  222. package/src/core/modules/migrate/mongo-state-store.ts +179 -11
  223. package/src/core/modules/system-setup/README.md +7 -2
  224. package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
  225. package/src/core/modules/tenant/README.md +21 -1
  226. package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
  227. package/src/core/modules/tenant/core-tenant.module.ts +8 -4
  228. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
  229. package/src/core/modules/tus/README.md +35 -2
  230. package/src/core/modules/tus/core-tus.controller.ts +64 -12
  231. package/src/core/modules/tus/core-tus.service.ts +358 -34
  232. package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
  233. package/src/core/modules/tus/tus-redis-locker.ts +139 -0
  234. package/src/core/modules/tus/tus.module.ts +78 -6
  235. package/src/core.module.ts +26 -1
  236. package/src/index.ts +10 -0
  237. package/src/main.ts +7 -1
  238. package/src/server/modules/file/file.controller.ts +7 -3
  239. package/src/server/modules/file/file.resolver.ts +5 -17
  240. package/src/server/modules/file/file.service.ts +42 -3
  241. package/src/server/modules/user/avatar.controller.ts +39 -9
  242. package/src/server/modules/user/user.model.ts +9 -3
  243. package/src/server/modules/user/user.module.ts +12 -3
  244. package/src/server/modules/user/user.service.ts +20 -17
  245. package/src/test/README.md +7 -2
  246. package/src/core/common/helpers/file.helper.spec.ts +0 -145
  247. package/src/core/common/helpers/logging.helper.spec.ts +0 -61
  248. package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
  249. package/src/core/common/services/brevo.service.spec.ts +0 -266
  250. package/src/core/modules/file/core-file.controller.spec.ts +0 -164
  251. package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
  252. package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
  253. package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
  254. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
  255. package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
  256. package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
  257. package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
  258. package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
  259. package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
  260. package/src/core/modules/migrate/helpers/migration.helper.spec.ts +0 -85
@@ -1,266 +0,0 @@
1
- import { Logger } from '@nestjs/common';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import type { Brevo, BrevoClient as RealBrevoClient } from '@getbrevo/brevo';
5
-
6
- import { BrevoService } from './brevo.service';
7
- import type { ConfigService } from './config.service';
8
-
9
- /**
10
- * Type-level contract against the REAL SDK.
11
- *
12
- * Everything below runs against a hand-written mock, so the mock alone could keep passing after a
13
- * breaking SDK upgrade. `sendMail()` is declared `Promise<unknown>`, which gives tsc nothing to
14
- * check either. These assertions close that gap: they compile against the installed
15
- * `@getbrevo/brevo`, and `pnpm run typecheck:tests` covers `src/**\/*.spec.ts`, so a v7 that
16
- * re-introduces a `.body` envelope or renames the request type fails the build here rather than
17
- * silently at runtime.
18
- */
19
- type SendFn = RealBrevoClient['transactionalEmails']['sendTransacEmail'];
20
- /** Awaiting the call must yield the payload itself — v3's `{ response, body }` envelope is gone. */
21
- type AssertNoEnvelope = Awaited<ReturnType<SendFn>> extends { body: unknown } ? never : true;
22
- /** The request type the service builds must still be assignable to the SDK's parameter. */
23
- type AssertRequestType = Brevo.SendTransacEmailRequest extends NonNullable<Parameters<SendFn>[0]> ? true : never;
24
- // Consumed by an assertion below so the contract cannot be dead-code-eliminated or linted away.
25
- const sdkContract: [AssertNoEnvelope, AssertRequestType] = [true, true];
26
-
27
- /**
28
- * Shared handles into the mocked SDK. `vi.hoisted` runs before the `vi.mock` factory below, so the
29
- * spies exist by the time the module graph is wired up.
30
- */
31
- const brevoMock = vi.hoisted(() => ({
32
- /** Every options object the client was constructed with, in call order. */
33
- clientOptions: [] as unknown[],
34
- sendTransacEmail: vi.fn(),
35
- }));
36
-
37
- vi.mock('@getbrevo/brevo', () => ({
38
- BrevoClient: class {
39
- transactionalEmails = { sendTransacEmail: brevoMock.sendTransacEmail };
40
-
41
- constructor(options: unknown) {
42
- brevoMock.clientOptions.push(options);
43
- }
44
- },
45
- }));
46
-
47
- const API_KEY = 'test-api-key';
48
- const SENDER = { email: 'noreply@test.com', name: 'Test Sender' };
49
-
50
- /** Matches the per-send `Idempotency-Key` header without pinning the random UUID. */
51
- const anyIdempotencyHeaders = { 'Idempotency-Key': expect.any(String) as unknown as string };
52
-
53
- /**
54
- * Minimal ConfigService double.
55
- *
56
- * `exclude` can be set independently on the mutable (`config`) and the frozen
57
- * (`configFastButReadOnly`) side so the tests can prove which one the service reads.
58
- */
59
- function makeConfigService(
60
- options: {
61
- exclude?: RegExp;
62
- frozenExclude?: RegExp;
63
- maxRetries?: number;
64
- throwOnError?: boolean;
65
- timeoutInSeconds?: number;
66
- withBrevo?: boolean;
67
- } = {},
68
- ): ConfigService {
69
- const { exclude, frozenExclude = exclude, maxRetries, throwOnError, timeoutInSeconds, withBrevo = true } = options;
70
- const base = { apiKey: API_KEY, maxRetries, sender: SENDER, throwOnError, timeoutInSeconds };
71
- return {
72
- config: withBrevo ? { brevo: { ...base, exclude } } : {},
73
- configFastButReadOnly: withBrevo ? { brevo: { ...base, exclude: frozenExclude } } : {},
74
- } as unknown as ConfigService;
75
- }
76
-
77
- describe('@getbrevo/brevo SDK contract', () => {
78
- it('still resolves the payload directly and accepts the request type we build', () => {
79
- // The assertion is the COMPILATION of `sdkContract` above, which `pnpm run typecheck:tests`
80
- // performs against the really installed SDK. This test body exists so the contract is also
81
- // referenced at runtime — otherwise it reads as dead code and invites deletion.
82
- expect(sdkContract).toEqual([true, true]);
83
- });
84
- });
85
-
86
- describe('BrevoService', () => {
87
- let loggerError: ReturnType<typeof vi.spyOn>;
88
-
89
- beforeEach(() => {
90
- brevoMock.sendTransacEmail.mockReset();
91
- brevoMock.clientOptions.length = 0;
92
- loggerError = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined);
93
- });
94
-
95
- afterEach(() => {
96
- // The spies above are never restored otherwise. File-level isolation contains it today, but a
97
- // leaked Logger spy is the kind of thing that only surfaces as an unrelated flake later.
98
- vi.restoreAllMocks();
99
- });
100
-
101
- describe('constructor', () => {
102
- it('throws when the Brevo configuration is missing', () => {
103
- expect(() => new BrevoService(makeConfigService({ withBrevo: false }))).toThrow('Brevo configuration not set!');
104
- });
105
-
106
- it('does NOT construct the SDK client eagerly', async () => {
107
- // The SDK is ~580 CommonJS modules and BrevoService is re-exported from the package barrel,
108
- // so a static import would put that cost on every consumer's cold start — including the
109
- // majority that never configure Brevo.
110
- const service = new BrevoService(makeConfigService());
111
- expect(brevoMock.clientOptions).toEqual([]);
112
-
113
- brevoMock.sendTransacEmail.mockResolvedValue({ messageId: '<x@brevo>' });
114
- await service.sendMail('user@example.com', 42);
115
- expect(brevoMock.clientOptions).toHaveLength(1);
116
- });
117
-
118
- it('creates the client with the configured API key and safe request limits', async () => {
119
- brevoMock.sendTransacEmail.mockResolvedValue({ messageId: '<x@brevo>' });
120
- const service = new BrevoService(makeConfigService());
121
- await service.sendMail('user@example.com', 42);
122
-
123
- // The SDK defaults to 2 retries honouring `Retry-After` with a 60 s cap PER attempt and to
124
- // no timeout at all. Both send methods are awaited inside request handlers, so those
125
- // defaults would let a rate-limited Brevo park a user-facing request for ~2 minutes.
126
- expect(brevoMock.clientOptions).toEqual([{ apiKey: API_KEY, maxRetries: 0, timeoutInSeconds: 10 }]);
127
- });
128
-
129
- it('honours configured retry and timeout overrides', async () => {
130
- brevoMock.sendTransacEmail.mockResolvedValue({ messageId: '<x@brevo>' });
131
- const service = new BrevoService(makeConfigService({ maxRetries: 3, timeoutInSeconds: 30 }));
132
- await service.sendMail('user@example.com', 42);
133
- expect(brevoMock.clientOptions).toEqual([{ apiKey: API_KEY, maxRetries: 3, timeoutInSeconds: 30 }]);
134
- });
135
-
136
- it('reuses the client across sends', async () => {
137
- brevoMock.sendTransacEmail.mockResolvedValue({ messageId: '<x@brevo>' });
138
- const service = new BrevoService(makeConfigService());
139
- await service.sendMail('user@example.com', 42);
140
- await service.sendMail('other@example.com', 42);
141
- expect(brevoMock.clientOptions).toHaveLength(1);
142
- });
143
- });
144
-
145
- describe('sendMail', () => {
146
- it('rejects incomplete input without calling the API', async () => {
147
- const service = new BrevoService(makeConfigService());
148
- await expect(service.sendMail('', 42)).resolves.toBe(false);
149
- await expect(service.sendMail('user@example.com', 0)).resolves.toBe(false);
150
- expect(brevoMock.sendTransacEmail).not.toHaveBeenCalled();
151
- });
152
-
153
- it('sends template, recipient and params, and returns the response unwrapped', async () => {
154
- const response = { messageId: '<mail-1@brevo>' };
155
- brevoMock.sendTransacEmail.mockResolvedValue(response);
156
- const service = new BrevoService(makeConfigService());
157
-
158
- // Identity check: v6 resolves the payload directly, there is no `.body` envelope any more
159
- await expect(service.sendMail('user@example.com', 42, { name: 'Test' })).resolves.toBe(response);
160
- expect(brevoMock.sendTransacEmail).toHaveBeenCalledWith({
161
- headers: anyIdempotencyHeaders,
162
- params: { name: 'Test' },
163
- templateId: 42,
164
- to: [{ email: 'user@example.com' }],
165
- });
166
- });
167
-
168
- it('sends a unique Idempotency-Key per call', async () => {
169
- // The SDK retries POSTs on 408/429/5xx. Without a key, a retry issued after a delivered
170
- // response whose reply was lost sends the mail twice.
171
- brevoMock.sendTransacEmail.mockResolvedValue({ messageId: '<x@brevo>' });
172
- const service = new BrevoService(makeConfigService());
173
- await service.sendMail('user@example.com', 42);
174
- await service.sendMail('user@example.com', 42);
175
-
176
- const keys = brevoMock.sendTransacEmail.mock.calls.map(
177
- ([request]) => (request as Brevo.SendTransacEmailRequest).headers?.['Idempotency-Key'],
178
- );
179
- expect(keys[0]).toBeTypeOf('string');
180
- expect(keys[0]).not.toBe(keys[1]);
181
- });
182
-
183
- it('skips excluded (test) recipients', async () => {
184
- const service = new BrevoService(makeConfigService({ exclude: /@test\.com$/i }));
185
- await expect(service.sendMail('user@test.com', 42)).resolves.toBe('TEST_USER!');
186
- expect(brevoMock.sendTransacEmail).not.toHaveBeenCalled();
187
- });
188
-
189
- it('reads the exclude pattern from the mutable config, not the frozen one', async () => {
190
- // A frozen RegExp carrying the `g` flag throws on `.test()` (it assigns `lastIndex`).
191
- // Reading `exclude` off `configFastButReadOnly` would therefore fail instead of excluding.
192
- const service = new BrevoService(
193
- makeConfigService({ exclude: /@test\.com$/i, frozenExclude: Object.freeze(/@test\.com$/gi) }),
194
- );
195
- await expect(service.sendMail('user@test.com', 42)).resolves.toBe('TEST_USER!');
196
- });
197
-
198
- it('returns null when the API call fails', async () => {
199
- brevoMock.sendTransacEmail.mockRejectedValue(new Error('Brevo down'));
200
- const service = new BrevoService(makeConfigService());
201
- await expect(service.sendMail('user@example.com', 42)).resolves.toBeNull();
202
- });
203
-
204
- it('logs the failure through the Nest logger', async () => {
205
- // The whole point of the sibling diagnostics work is that silent failures cost debugging
206
- // sessions. Asserting only the `null` return would let the observability half regress.
207
- brevoMock.sendTransacEmail.mockRejectedValue(new Error('Brevo down'));
208
- const service = new BrevoService(makeConfigService());
209
- await service.sendMail('user@example.com', 42);
210
- expect(loggerError).toHaveBeenCalledWith(expect.stringContaining('Brevo down'));
211
- });
212
-
213
- it('rethrows when throwOnError is enabled', async () => {
214
- const service = new BrevoService(makeConfigService({ throwOnError: true }));
215
- brevoMock.sendTransacEmail.mockRejectedValue(new Error('Brevo down'));
216
- await expect(service.sendMail('user@example.com', 42)).rejects.toThrow('Brevo down');
217
- });
218
- });
219
-
220
- describe('sendHtmlMail', () => {
221
- it('rejects incomplete input without calling the API', async () => {
222
- const service = new BrevoService(makeConfigService());
223
- await expect(service.sendHtmlMail('', 'Subject', '<p>Hi</p>')).resolves.toBe(false);
224
- await expect(service.sendHtmlMail('user@example.com', '', '<p>Hi</p>')).resolves.toBe(false);
225
- await expect(service.sendHtmlMail('user@example.com', 'Subject', '')).resolves.toBe(false);
226
- expect(brevoMock.sendTransacEmail).not.toHaveBeenCalled();
227
- });
228
-
229
- it('sends html, subject and the configured sender, and returns the response unwrapped', async () => {
230
- const response = { messageId: '<mail-2@brevo>' };
231
- brevoMock.sendTransacEmail.mockResolvedValue(response);
232
- const service = new BrevoService(makeConfigService());
233
-
234
- await expect(
235
- service.sendHtmlMail('user@example.com', 'Subject', '<p>Hi</p>', { params: { code: '123' } }),
236
- ).resolves.toBe(response);
237
- expect(brevoMock.sendTransacEmail).toHaveBeenCalledWith({
238
- headers: anyIdempotencyHeaders,
239
- htmlContent: '<p>Hi</p>',
240
- params: { code: '123' },
241
- sender: SENDER,
242
- subject: 'Subject',
243
- to: [{ email: 'user@example.com' }],
244
- });
245
- });
246
-
247
- it('skips excluded (test) recipients', async () => {
248
- const service = new BrevoService(makeConfigService({ exclude: /@test\.com$/i }));
249
- await expect(service.sendHtmlMail('user@test.com', 'Subject', '<p>Hi</p>')).resolves.toBe('TEST_USER!');
250
- expect(brevoMock.sendTransacEmail).not.toHaveBeenCalled();
251
- });
252
-
253
- it('returns null when the API call fails', async () => {
254
- brevoMock.sendTransacEmail.mockRejectedValue(new Error('Brevo down'));
255
- const service = new BrevoService(makeConfigService());
256
- await expect(service.sendHtmlMail('user@example.com', 'Subject', '<p>Hi</p>')).resolves.toBeNull();
257
- });
258
-
259
- it('logs the failure through the Nest logger', async () => {
260
- brevoMock.sendTransacEmail.mockRejectedValue(new Error('Brevo down'));
261
- const service = new BrevoService(makeConfigService());
262
- await service.sendHtmlMail('user@example.com', 'Subject', '<p>Hi</p>');
263
- expect(loggerError).toHaveBeenCalledWith(expect.stringContaining('Brevo down'));
264
- });
265
- });
266
- });
@@ -1,164 +0,0 @@
1
- import { NotFoundException } from '@nestjs/common';
2
- import type { Response } from 'express';
3
- import { PassThrough, Readable } from 'stream';
4
- import { describe, expect, it } from 'vitest';
5
-
6
- import type { CoreFileService } from './core-file.service';
7
- import { CoreFileController, pipeFileToResponse } from './core-file.controller';
8
-
9
- /**
10
- * Build a response stub that records what the handler did to it.
11
- *
12
- * A real `Response` is not needed here — what is under test is the decision
13
- * (status vs. destroy), not Express. Writing it out as a stub also makes the
14
- * "headers already sent" case reachable, which is awkward to force otherwise.
15
- *
16
- * What this stub CANNOT show is how Express itself treats headers: `res.json()`
17
- * only defaults `Content-Type` when none is set, so a stub whose `json()` merely
18
- * records a body can never reveal a stale one. That property is pinned at the
19
- * HTTP level in `tests/file.e2e-spec.ts`; here we assert the removal list.
20
- */
21
- function responseStub(headersSent = false) {
22
- const sink = new PassThrough();
23
- const calls = {
24
- destroyed: false,
25
- json: undefined as unknown,
26
- removed: [] as string[],
27
- set: {} as Record<string, string>,
28
- status: 0,
29
- };
30
-
31
- const res = Object.assign(sink, {
32
- destroy: () => {
33
- calls.destroyed = true;
34
- return res;
35
- },
36
- headersSent,
37
- json: (body: unknown) => {
38
- calls.json = body;
39
- return res;
40
- },
41
- removeHeader: (name: string) => {
42
- calls.removed.push(name);
43
- },
44
- setHeader: (name: string, value: string) => {
45
- calls.set[name] = value;
46
- return res;
47
- },
48
- status: (code: number) => {
49
- calls.status = code;
50
- return res;
51
- },
52
- }) as unknown as Response;
53
-
54
- return { calls, res };
55
- }
56
-
57
- describe('pipeFileToResponse', () => {
58
- it('turns a read error into 404 while nothing has been written yet', async () => {
59
- // A GridFS record and its bytes are two separate writes, so the record can
60
- // outlive the chunks. GridFS reports that on the stream, asynchronously.
61
- // A bare `stream.pipe(res)` installs no error handler, so the error goes
62
- // unhandled, Node destroys the socket mid-response, and any reverse proxy
63
- // in front reports 502 Bad Gateway — "the server is down", which is the one
64
- // diagnosis that is wrong while every other route answers normally.
65
- const stream = new Readable({ read() {} });
66
- const { calls, res } = responseStub(false);
67
-
68
- pipeFileToResponse(stream, res);
69
- stream.emit('error', new Error('FileNotFound: file 0123456789ab was not found'));
70
- await new Promise((resolve) => setImmediate(resolve));
71
-
72
- expect(calls.status).toBe(404);
73
- expect(calls.json).toMatchObject({ statusCode: 404 });
74
- expect(calls.destroyed).toBe(false);
75
- });
76
-
77
- it('drops every header that describes the file, not just the download one', async () => {
78
- // Each of these would otherwise survive onto the error response:
79
- // `Content-Disposition` offers a JSON error body as a file to save, a long
80
- // `Cache-Control` lets the client cache the failure, `ETag` describes bytes
81
- // that were never sent — and `Content-Type` makes an ofetch/`$fetch` client
82
- // parse the JSON as an image and lose the message entirely.
83
- const stream = new Readable({ read() {} });
84
- const { calls, res } = responseStub(false);
85
-
86
- pipeFileToResponse(stream, res);
87
- stream.emit('error', new Error('chunks missing'));
88
- await new Promise((resolve) => setImmediate(resolve));
89
-
90
- expect(calls.removed).toEqual(
91
- expect.arrayContaining(['Cache-Control', 'Content-Disposition', 'Content-Type', 'ETag']),
92
- );
93
- expect(calls.set['X-Content-Type-Options']).toBe('nosniff');
94
- });
95
-
96
- it('closes the connection once bytes are already on the wire', async () => {
97
- // Past the first byte there is no status left to send. Dropping the
98
- // connection is the only remaining signal — and it is the correct one: it
99
- // is what a truncated transfer looks like, so the client does not cache a
100
- // half file as if it were complete.
101
- const stream = new Readable({ read() {} });
102
- const { calls, res } = responseStub(true);
103
-
104
- pipeFileToResponse(stream, res);
105
- stream.emit('error', new Error('connection lost mid-stream'));
106
- await new Promise((resolve) => setImmediate(resolve));
107
-
108
- expect(calls.destroyed).toBe(true);
109
- expect(calls.status).toBe(0);
110
- });
111
-
112
- it('answers a refused download exactly like an unknown id', async () => {
113
- // `getFileStream()` returns null when the service's own `checkRights()`
114
- // refuses — a branch only a CONSUMER project can reach, since the framework's
115
- // base implementation always returns true. That is precisely why the
116
- // framework has to pin it: consumers cannot, and if the two answers ever
117
- // diverge (403 here, 404 there, or different messages) the endpoint becomes
118
- // an existence oracle for files the caller may not read.
119
- // Invoked through the prototype rather than a subclass: `CoreFileController`
120
- // has a protected constructor (it is abstract by design), and a subclass added
121
- // only to widen that visibility is exactly what `no-useless-constructor`
122
- // strips — leaving code that no longer compiles.
123
- const download = (service: Partial<CoreFileService>, res: Response) =>
124
- (CoreFileController.prototype.getFileById as (this: unknown, id: string, res: Response) => Promise<unknown>).call(
125
- { fileService: service },
126
- 'abc',
127
- res,
128
- );
129
-
130
- const file = { contentType: 'image/png', filename: 'secret.png', id: 'abc' };
131
- const { res } = responseStub(false);
132
-
133
- const refusedErr = await download(
134
- { getFileInfo: async () => file, getFileStream: async () => null } as unknown as Partial<CoreFileService>,
135
- res,
136
- ).catch((e: unknown) => e);
137
- const unknownErr = await download(
138
- { getFileInfo: async () => null, getFileStream: async () => null } as unknown as Partial<CoreFileService>,
139
- res,
140
- ).catch((e: unknown) => e);
141
-
142
- expect(refusedErr).toBeInstanceOf(NotFoundException);
143
- expect(unknownErr).toBeInstanceOf(NotFoundException);
144
- // Byte-identical answers: same status, same message.
145
- expect((refusedErr as NotFoundException).getStatus()).toBe((unknownErr as NotFoundException).getStatus());
146
- expect((refusedErr as NotFoundException).getResponse()).toEqual((unknownErr as NotFoundException).getResponse());
147
- });
148
-
149
- it('destroys the source stream when the client goes away', async () => {
150
- // `pipe()` only ever unpipes the DESTINATION; it never destroys the source.
151
- // On a public download route an aborted request would therefore leak the
152
- // GridFS read stream and its server-side cursor until the cursor timeout.
153
- const stream = new Readable({ read() {} });
154
- const { res } = responseStub(false);
155
-
156
- pipeFileToResponse(stream, res);
157
- expect(stream.destroyed).toBe(false);
158
-
159
- res.emit('close');
160
- await new Promise((resolve) => setImmediate(resolve));
161
-
162
- expect(stream.destroyed).toBe(true);
163
- });
164
- });
@@ -1,108 +0,0 @@
1
- import { PATH_METADATA } from '@nestjs/common/constants';
2
- import { describe, expect, it } from 'vitest';
3
-
4
- import { CoreHubController } from './core-hub.controller';
5
- import { CoreHubModule } from './core-hub.module';
6
-
7
- const ctx = { env: 'local', version: '1.0.0' };
8
-
9
- describe('CoreHubModule.forRoot', () => {
10
- it('builds a dynamic module with the hub controller', () => {
11
- const mod = CoreHubModule.forRoot({ config: {}, ctx });
12
- expect(mod.module).toBe(CoreHubModule);
13
- expect(mod.controllers?.length).toBeGreaterThan(0);
14
- });
15
-
16
- it('becomes global and provides the email-capture binding when the mailbox is enabled', () => {
17
- const mod = CoreHubModule.forRoot({ config: { mailbox: { mode: 'capture' } }, ctx });
18
- expect(mod.global).toBe(true);
19
- });
20
-
21
- it('is not global when the mailbox is disabled', () => {
22
- const mod = CoreHubModule.forRoot({ config: {}, ctx });
23
- expect(mod.global).toBeFalsy();
24
- });
25
-
26
- it('throws when mailbox capture mode is used in production (silent mail suppression guard)', () => {
27
- expect(() =>
28
- CoreHubModule.forRoot({ config: { mailbox: { mode: 'capture' } }, ctx: { env: 'production', version: '1' } }),
29
- ).toThrow(/capture.*not permitted/i);
30
- });
31
-
32
- it('allows mailbox copy mode in production', () => {
33
- expect(() =>
34
- CoreHubModule.forRoot({ config: { mailbox: { mode: 'copy' } }, ctx: { env: 'production', version: '1' } }),
35
- ).not.toThrow();
36
- });
37
-
38
- describe('public-access guard (roles: false in a reachable env)', () => {
39
- it('throws when roles:false in production without acknowledgment', () => {
40
- expect(() =>
41
- CoreHubModule.forRoot({ config: { roles: false }, ctx: { env: 'production', version: '1' } }),
42
- ).toThrow(/roles: false.*not permitted/i);
43
- });
44
-
45
- it('throws when roles:false in staging without acknowledgment', () => {
46
- expect(() => CoreHubModule.forRoot({ config: { roles: false }, ctx: { env: 'staging', version: '1' } })).toThrow(
47
- /not permitted/i,
48
- );
49
- });
50
-
51
- it('throws for CUSTOM reachable env names too (fail-safe — not just production/staging)', () => {
52
- // Regression guard for the exact-string bypass: prod / live / preprod / production-eu / staging-2
53
- // must all be treated as reachable.
54
- for (const env of ['prod', 'live', 'preprod', 'production-eu', 'staging-2']) {
55
- expect(() => CoreHubModule.forRoot({ config: { roles: false }, ctx: { env, version: '1' } })).toThrow(
56
- /not permitted/i,
57
- );
58
- }
59
- });
60
-
61
- it('allows roles:false in production ONLY with explicit allowPublicAccessInProduction', () => {
62
- expect(() =>
63
- CoreHubModule.forRoot({
64
- config: { allowPublicAccessInProduction: true, roles: false },
65
- ctx: { env: 'production', version: '1' },
66
- }),
67
- ).not.toThrow();
68
- });
69
-
70
- it('allows roles:false in non-reachable envs (local, development)', () => {
71
- expect(() => CoreHubModule.forRoot({ config: { roles: false }, ctx })).not.toThrow(); // ctx = local
72
- expect(() =>
73
- CoreHubModule.forRoot({ config: { roles: false }, ctx: { env: 'development', version: '1' } }),
74
- ).not.toThrow();
75
- });
76
-
77
- it('allows the default ADMIN gate in production (no roles set)', () => {
78
- expect(() => CoreHubModule.forRoot({ config: {}, ctx: { env: 'production', version: '1' } })).not.toThrow();
79
- });
80
- });
81
-
82
- describe('runtime path/roles metadata (shared controller class)', () => {
83
- it('writes the configured base path onto the controller class', () => {
84
- CoreHubModule.forRoot({ config: { path: 'cockpit' }, ctx });
85
- expect(Reflect.getMetadata(PATH_METADATA, CoreHubController)).toBe('cockpit');
86
- });
87
-
88
- it('fully overwrites stale metadata on a second forRoot (no cross-instance pollution)', () => {
89
- // The controller class is shared across app instances in the same process (parallel tests). A
90
- // second forRoot must overwrite the first, never leak the previous path/roles.
91
- CoreHubModule.forRoot({ config: { path: 'first', roles: 'admin' }, ctx });
92
- expect(Reflect.getMetadata(PATH_METADATA, CoreHubController)).toBe('first');
93
- expect(Reflect.getMetadata('roles', CoreHubController)).toEqual(['admin']);
94
-
95
- CoreHubModule.forRoot({ config: { path: 'second', roles: ['ops', 'admin'] }, ctx });
96
- expect(Reflect.getMetadata(PATH_METADATA, CoreHubController)).toBe('second');
97
- expect(Reflect.getMetadata('roles', CoreHubController)).toEqual(['ops', 'admin']);
98
- });
99
-
100
- it('clears the roles metadata when roles: false (public opt-out)', () => {
101
- CoreHubModule.forRoot({ config: { roles: 'admin' }, ctx });
102
- expect(Reflect.getMetadata('roles', CoreHubController)).toEqual(['admin']);
103
-
104
- CoreHubModule.forRoot({ config: { roles: false }, ctx });
105
- expect(Reflect.getMetadata('roles', CoreHubController)).toBeUndefined();
106
- });
107
- });
108
- });
@@ -1,48 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { normalizeCommandShape } from './hub-command-shape.helper';
4
-
5
- describe('normalizeCommandShape', () => {
6
- it('replaces scalar values with a placeholder but keeps keys', () => {
7
- const out = normalizeCommandShape({ email: 'alice@example.com', age: 30, active: true });
8
- expect(out).toEqual({ active: '?', age: '?', email: '?' });
9
- });
10
-
11
- it('keeps Mongo operators as keys (they are structure, not data)', () => {
12
- const out = normalizeCommandShape({ age: { $gte: 18, $lt: 65 } });
13
- expect(out).toEqual({ age: { $gte: '?', $lt: '?' } });
14
- });
15
-
16
- it('collapses scalar arrays ($in) to a single placeholder', () => {
17
- const out = normalizeCommandShape({ id: { $in: [1, 2, 3, 4] } });
18
- expect(out).toEqual({ id: { $in: ['?'] } });
19
- });
20
-
21
- it('keeps per-stage shape for pipeline-like object arrays', () => {
22
- const out = normalizeCommandShape([{ $match: { x: 1 } }, { $group: { _id: '$y' } }]) as unknown[];
23
- expect(out).toHaveLength(2);
24
- expect(out[0]).toEqual({ $match: { x: '?' } });
25
- });
26
-
27
- it('does NOT leak the original values (privacy)', () => {
28
- const secret = 'super-secret@example.com';
29
- const out = normalizeCommandShape({ filter: { email: secret } });
30
- expect(JSON.stringify(out)).not.toContain(secret);
31
- });
32
-
33
- it('caps recursion depth', () => {
34
- let deep: any = 'leaf';
35
- for (let i = 0; i < 20; i++) {
36
- deep = { nested: deep };
37
- }
38
- const out = JSON.stringify(normalizeCommandShape(deep));
39
- // Must terminate and not blow the stack; the sentinel marks the cut.
40
- expect(out).toContain('…');
41
- });
42
-
43
- it('produces identical output for identical query shapes (the N+1 template key)', () => {
44
- const a = normalizeCommandShape({ filter: { email: 'a@x.com' }, limit: 1 });
45
- const b = normalizeCommandShape({ filter: { email: 'b@y.com' }, limit: 999 });
46
- expect(JSON.stringify(a)).toBe(JSON.stringify(b));
47
- });
48
- });
@@ -1,67 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { maskConfigDeep } from './hub-mask.helper';
4
-
5
- describe('maskConfigDeep', () => {
6
- it('masks values whose key matches the secret pattern', () => {
7
- const out = maskConfigDeep({
8
- jwt: { secret: 'SENTINEL_JWT', refresh: { secret: 'SENTINEL_REFRESH' } },
9
- port: 3000,
10
- });
11
-
12
- expect(out.jwt.secret).toBe('***');
13
- expect(out.jwt.refresh.secret).toBe('***');
14
- expect(out.port).toBe(3000);
15
- });
16
-
17
- it('masks a broad set of secret-ish key names', () => {
18
- const out = maskConfigDeep({
19
- apiKey: 'AK',
20
- betterAuth: { secret: 'BA' },
21
- encryptionSecret: 'ENC',
22
- password: 'PW',
23
- privateKey: 'PK',
24
- token: 'TK',
25
- });
26
-
27
- for (const value of Object.values(out)) {
28
- const flat = JSON.stringify(value);
29
- expect(flat).not.toMatch(/AK|BA|ENC|PW|PK|TK/);
30
- }
31
- });
32
-
33
- it('deep-clones — the input object is never mutated', () => {
34
- const input = { jwt: { secret: 'SENTINEL' }, nested: { arr: [{ token: 'T' }] } };
35
- const out = maskConfigDeep(input);
36
-
37
- expect(input.jwt.secret).toBe('SENTINEL');
38
- expect(input.nested.arr[0].token).toBe('T');
39
- expect(out.jwt.secret).toBe('***');
40
- expect(out.nested.arr[0].token).toBe('***');
41
- });
42
-
43
- it('masks credentials embedded in URI-shaped values', () => {
44
- const out = maskConfigDeep({ mongoose: { uri: 'mongodb://user:secretpw@host:27017/db' } });
45
-
46
- expect(out.mongoose.uri).not.toContain('secretpw');
47
- expect(out.mongoose.uri).toContain('mongodb://');
48
- expect(out.mongoose.uri).toContain('host:27017');
49
- });
50
-
51
- it('honors additional explicit secret field names', () => {
52
- const out = maskConfigDeep({ customField: 'HIDE_ME', other: 'keep' }, ['customField']);
53
-
54
- expect(out.customField).toBe('***');
55
- expect(out.other).toBe('keep');
56
- });
57
-
58
- it('passes through null, numbers, booleans and dates', () => {
59
- const date = new Date('2026-01-01T00:00:00.000Z');
60
- const out = maskConfigDeep({ a: null, b: 42, c: true, d: date });
61
-
62
- expect(out.a).toBeNull();
63
- expect(out.b).toBe(42);
64
- expect(out.c).toBe(true);
65
- expect(out.d).toBeInstanceOf(Date);
66
- });
67
- });