@lenne.tech/nest-server 11.32.4 → 11.33.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.
- package/.claude/rules/architecture.md +16 -0
- package/.claude/rules/configurable-features.md +11 -3
- package/.claude/rules/testing.md +101 -1
- package/CLAUDE.md +5 -0
- package/FRAMEWORK-API.md +58 -3
- package/dist/config.env.js +6 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
- package/dist/core/common/helpers/content-disposition.helper.js +14 -0
- package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
- package/dist/core/common/helpers/file.helper.d.ts +7 -0
- package/dist/core/common/helpers/file.helper.js +21 -4
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
- package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
- package/dist/core/common/helpers/project-name.helper.js +38 -0
- package/dist/core/common/helpers/project-name.helper.js.map +1 -0
- package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
- package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
- package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
- package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
- package/dist/core/common/services/core-cron-jobs.service.js +278 -27
- package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
- package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
- package/dist/core/common/services/core-redis-pubsub.js +112 -0
- package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
- package/dist/core/common/services/core-redis.service.d.ts +25 -0
- package/dist/core/common/services/core-redis.service.js +154 -0
- package/dist/core/common/services/core-redis.service.js.map +1 -0
- package/dist/core/common/services/core-s3.service.d.ts +51 -0
- package/dist/core/common/services/core-s3.service.js +261 -0
- package/dist/core/common/services/core-s3.service.js.map +1 -0
- package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
- package/dist/core/common/services/rate-limit-store.d.ts +61 -0
- package/dist/core/common/services/rate-limit-store.js +235 -0
- package/dist/core/common/services/rate-limit-store.js.map +1 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
- package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
- package/dist/core/modules/ai/services/core-ai.service.js +24 -20
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +4 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/file/core-file.controller.d.ts +11 -2
- package/dist/core/modules/file/core-file.controller.js +52 -15
- package/dist/core/modules/file/core-file.controller.js.map +1 -1
- package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
- package/dist/core/modules/file/core-file.resolver.js +23 -16
- package/dist/core/modules/file/core-file.resolver.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +41 -7
- package/dist/core/modules/file/core-file.service.js +228 -8
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
- package/dist/core/modules/file/file-metadata.helper.js +33 -0
- package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
- package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
- package/dist/core/modules/file/file-roles.helper.js +53 -0
- package/dist/core/modules/file/file-roles.helper.js.map +1 -0
- package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
- package/dist/core/modules/file/file-storage.helper.js +65 -0
- package/dist/core/modules/file/file-storage.helper.js.map +1 -0
- package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
- package/dist/core/modules/file/filesystem-file.helper.js +77 -0
- package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
- package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
- package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
- package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
- package/dist/core/modules/file/s3-file.helper.js +81 -0
- package/dist/core/modules/file/s3-file.helper.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
- package/dist/core/modules/hub/core-hub.controller.js +24 -22
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
- package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
- package/dist/core/modules/hub/hub-buffer.js +101 -0
- package/dist/core/modules/hub/hub-buffer.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
- package/dist/core/modules/migrate/helpers/migration.helper.d.ts +2 -6
- package/dist/core/modules/migrate/helpers/migration.helper.js +4 -4
- package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
- package/dist/core/modules/migrate/migration-runner.js +4 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
- package/dist/core/modules/migrate/mongo-state-store.js +98 -9
- package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
- package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
- package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
- package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.js +3 -4
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
- package/dist/core/modules/tus/core-tus.controller.js +29 -3
- package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
- package/dist/core/modules/tus/core-tus.service.js +182 -28
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
- package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
- package/dist/core/modules/tus/tus-redis-locker.js +87 -0
- package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
- package/dist/core/modules/tus/tus.module.d.ts +1 -0
- package/dist/core/modules/tus/tus.module.js +30 -4
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/core.module.js +18 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +2 -1
- package/dist/main.js.map +1 -1
- package/dist/server/modules/file/file.controller.js +2 -2
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.resolver.js +3 -14
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/file.service.d.ts +10 -2
- package/dist/server/modules/file/file.service.js +23 -3
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/server/modules/user/avatar.controller.d.ts +5 -1
- package/dist/server/modules/user/avatar.controller.js +26 -10
- package/dist/server/modules/user/avatar.controller.js.map +1 -1
- package/dist/server/modules/user/user.model.js +3 -2
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.module.js +6 -2
- package/dist/server/modules/user/user.module.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +1 -1
- package/dist/server/modules/user/user.service.js +6 -13
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +57 -8
- package/docs/security-overrides.md +21 -3
- package/migration-guides/11.32.3-to-11.32.4.md +15 -3
- package/migration-guides/11.32.x-to-11.33.x.md +1584 -0
- package/package.json +74 -42
- package/src/config.env.ts +45 -0
- package/src/core/common/helpers/content-disposition.helper.ts +90 -0
- package/src/core/common/helpers/file.helper.ts +53 -10
- package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
- package/src/core/common/helpers/project-name.helper.ts +71 -0
- package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
- package/src/core/common/interfaces/server-options.interface.ts +439 -2
- package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
- package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
- package/src/core/common/services/core-cron-jobs.service.ts +589 -47
- package/src/core/common/services/core-redis-pubsub.ts +183 -0
- package/src/core/common/services/core-redis.service.ts +249 -0
- package/src/core/common/services/core-s3.service.ts +465 -0
- package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
- package/src/core/common/services/rate-limit-store.ts +492 -0
- package/src/core/modules/ai/README.md +25 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
- package/src/core/modules/ai/services/core-ai.service.ts +33 -24
- package/src/core/modules/auth/core-auth.module.ts +7 -1
- package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
- package/src/core/modules/file/INTEGRATION-CHECKLIST.md +120 -0
- package/src/core/modules/file/README.md +308 -53
- package/src/core/modules/file/core-file.controller.ts +168 -12
- package/src/core/modules/file/core-file.resolver.ts +46 -12
- package/src/core/modules/file/core-file.service.ts +517 -17
- package/src/core/modules/file/file-metadata.helper.ts +142 -0
- package/src/core/modules/file/file-roles.helper.ts +110 -0
- package/src/core/modules/file/file-storage.helper.ts +163 -0
- package/src/core/modules/file/filesystem-file.helper.ts +184 -0
- package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
- package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
- package/src/core/modules/file/s3-file.helper.ts +209 -0
- package/src/core/modules/hub/README.md +48 -0
- package/src/core/modules/hub/core-hub.controller.ts +18 -16
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
- package/src/core/modules/hub/hub-buffer.ts +198 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
- package/src/core/modules/migrate/README.md +33 -6
- package/src/core/modules/migrate/helpers/migration.helper.ts +13 -6
- package/src/core/modules/migrate/migration-runner.ts +10 -1
- package/src/core/modules/migrate/mongo-state-store.ts +179 -11
- package/src/core/modules/system-setup/README.md +7 -2
- package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
- package/src/core/modules/tenant/README.md +21 -1
- package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
- package/src/core/modules/tenant/core-tenant.module.ts +8 -4
- package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
- package/src/core/modules/tus/README.md +35 -2
- package/src/core/modules/tus/core-tus.controller.ts +64 -12
- package/src/core/modules/tus/core-tus.service.ts +358 -34
- package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
- package/src/core/modules/tus/tus-redis-locker.ts +139 -0
- package/src/core/modules/tus/tus.module.ts +78 -6
- package/src/core.module.ts +26 -1
- package/src/index.ts +10 -0
- package/src/main.ts +7 -1
- package/src/server/modules/file/file.controller.ts +14 -5
- package/src/server/modules/file/file.resolver.ts +11 -19
- package/src/server/modules/file/file.service.ts +89 -4
- package/src/server/modules/user/avatar.controller.ts +46 -9
- package/src/server/modules/user/user.model.ts +9 -3
- package/src/server/modules/user/user.module.ts +12 -3
- package/src/server/modules/user/user.service.ts +20 -17
- package/src/test/README.md +7 -2
- package/src/core/common/helpers/file.helper.spec.ts +0 -145
- package/src/core/common/helpers/logging.helper.spec.ts +0 -61
- package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
- package/src/core/common/services/brevo.service.spec.ts +0 -266
- package/src/core/modules/file/core-file.controller.spec.ts +0 -164
- package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
- package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
- package/src/core/modules/migrate/helpers/migration.helper.spec.ts +0 -85
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
// Value and type imports are deliberately split into two statements. The lt CLI's vendor
|
|
4
|
-
// conversion drops an INLINE `type` specifier from a mixed import — `{ buildErDiagram, type
|
|
5
|
-
// HubModelDescriptor }` arrives in a vendored project as `{ buildErDiagram }`, and the file then
|
|
6
|
-
// fails to compile with TS2304. Keeping the two forms separate survives the conversion.
|
|
7
|
-
// (The CLI defect is tracked separately; this keeps src/core/ vendor-safe meanwhile.)
|
|
8
|
-
import type { HubModelDescriptor } from './hub-mermaid.helper';
|
|
9
|
-
|
|
10
|
-
import { buildErDiagram } from './hub-mermaid.helper';
|
|
11
|
-
|
|
12
|
-
describe('buildErDiagram', () => {
|
|
13
|
-
const models: HubModelDescriptor[] = [
|
|
14
|
-
{
|
|
15
|
-
fields: [
|
|
16
|
-
{ name: 'email', type: 'String' },
|
|
17
|
-
{ name: 'age', type: 'Number' },
|
|
18
|
-
{ name: 'company', ref: 'Company', type: 'ObjectId' },
|
|
19
|
-
],
|
|
20
|
-
name: 'User',
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
fields: [{ name: 'name', type: 'String' }],
|
|
24
|
-
name: 'Company',
|
|
25
|
-
},
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
it('opens an erDiagram block', () => {
|
|
29
|
-
const out = buildErDiagram(models);
|
|
30
|
-
expect(out.startsWith('erDiagram')).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('emits one entity per model with its fields and types', () => {
|
|
34
|
-
const out = buildErDiagram(models);
|
|
35
|
-
expect(out).toContain('User {');
|
|
36
|
-
expect(out).toContain('Company {');
|
|
37
|
-
expect(out).toContain('String email');
|
|
38
|
-
expect(out).toContain('Number age');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('renders a relationship for a ref field', () => {
|
|
42
|
-
const out = buildErDiagram(models);
|
|
43
|
-
expect(out).toMatch(/User \|\|--o\{ Company : company/);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('ignores refs pointing at models that are not present', () => {
|
|
47
|
-
const out = buildErDiagram([{ fields: [{ name: 'ghost', ref: 'Missing', type: 'ObjectId' }], name: 'Solo' }]);
|
|
48
|
-
expect(out).toContain('Solo {');
|
|
49
|
-
expect(out).not.toContain('Missing');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('sanitizes field/entity names so the diagram stays valid', () => {
|
|
53
|
-
const out = buildErDiagram([{ fields: [{ name: 'weird-field.name', type: 'String' }], name: 'Odd Name' }]);
|
|
54
|
-
expect(out).not.toContain('weird-field.name');
|
|
55
|
-
expect(out).not.toContain('Odd Name {');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('handles an empty model list without throwing', () => {
|
|
59
|
-
expect(buildErDiagram([])).toBe('erDiagram');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
buildHubCsp,
|
|
5
|
-
buildHubSecurityHeaders,
|
|
6
|
-
escapeHtml,
|
|
7
|
-
escapeJsString,
|
|
8
|
-
generateNonce,
|
|
9
|
-
injectNonce,
|
|
10
|
-
} from './hub-shell.helper';
|
|
11
|
-
|
|
12
|
-
describe('hub-shell.helper', () => {
|
|
13
|
-
describe('escapeHtml', () => {
|
|
14
|
-
it('escapes all five HTML-significant characters', () => {
|
|
15
|
-
expect(escapeHtml(`<img src="x" onerror='y'>&`)).toBe('<img src="x" onerror='y'>&');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('neutralizes a script-tag XSS payload', () => {
|
|
19
|
-
const out = escapeHtml('<script>alert(1)</script>');
|
|
20
|
-
expect(out).not.toContain('<script>');
|
|
21
|
-
expect(out).toContain('<script>');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('handles empty / non-string-ish input', () => {
|
|
25
|
-
expect(escapeHtml('')).toBe('');
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('escapeJsString', () => {
|
|
30
|
-
it('escapes characters that would break out of a JS string / script context', () => {
|
|
31
|
-
const out = escapeJsString(`a'b\\c</script>`);
|
|
32
|
-
expect(out).not.toContain('</script>');
|
|
33
|
-
expect(out).toContain("\\'");
|
|
34
|
-
expect(out).toContain('\\\\');
|
|
35
|
-
// The closing angle bracket must be escaped so `</script>` cannot terminate the tag.
|
|
36
|
-
expect(out).toContain('\\u003c');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('escapes CR / LF into their JS escapes', () => {
|
|
40
|
-
const out = escapeJsString('line1\r\nline2');
|
|
41
|
-
expect(out).toBe('line1\\r\\nline2');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('escapes U+2028 / U+2029, which are line terminators inside a JS string literal', () => {
|
|
45
|
-
// Built via fromCharCode so this test file stays pure ASCII (a literal separator would itself
|
|
46
|
-
// terminate the source line). A raw U+2028 in a `'...'` literal is a syntax error at runtime.
|
|
47
|
-
const lineSep = String.fromCharCode(0x2028);
|
|
48
|
-
const paraSep = String.fromCharCode(0x2029);
|
|
49
|
-
const out = escapeJsString(`before${lineSep}mid${paraSep}after`);
|
|
50
|
-
expect(out).toContain('\\u2028');
|
|
51
|
-
expect(out).toContain('\\u2029');
|
|
52
|
-
expect(out).not.toContain(lineSep);
|
|
53
|
-
expect(out).not.toContain(paraSep);
|
|
54
|
-
// The escaped result must be a valid single-line JS string literal.
|
|
55
|
-
expect(() => new Function(`return '${out}';`)()).not.toThrow();
|
|
56
|
-
expect(new Function(`return '${out}';`)()).toBe(`before${lineSep}mid${paraSep}after`);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('generateNonce', () => {
|
|
61
|
-
it('produces a non-empty base64 nonce that differs each call', () => {
|
|
62
|
-
const a = generateNonce();
|
|
63
|
-
const b = generateNonce();
|
|
64
|
-
expect(a.length).toBeGreaterThan(10);
|
|
65
|
-
expect(a).not.toBe(b);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('injectNonce', () => {
|
|
70
|
-
it('replaces every __CSP_NONCE__ placeholder', () => {
|
|
71
|
-
const html = `<style nonce="__CSP_NONCE__"></style><script nonce="__CSP_NONCE__"></script>`;
|
|
72
|
-
const out = injectNonce(html, 'abc123');
|
|
73
|
-
expect(out).not.toContain('__CSP_NONCE__');
|
|
74
|
-
expect(out).toContain('nonce="abc123"');
|
|
75
|
-
expect((out.match(/abc123/g) ?? []).length).toBe(2);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('buildHubCsp', () => {
|
|
80
|
-
it('is a strict, self-contained policy pinned to the nonce', () => {
|
|
81
|
-
const csp = buildHubCsp('nonce123');
|
|
82
|
-
expect(csp).toContain("default-src 'none'");
|
|
83
|
-
expect(csp).toContain("script-src 'nonce-nonce123'");
|
|
84
|
-
expect(csp).toContain("frame-ancestors 'none'");
|
|
85
|
-
expect(csp).not.toContain('unsafe-inline');
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('buildHubSecurityHeaders', () => {
|
|
90
|
-
it('returns the strict header set with no-store for HTML by default', () => {
|
|
91
|
-
const headers = buildHubSecurityHeaders('n1');
|
|
92
|
-
expect(headers['Content-Security-Policy']).toContain('nonce-n1');
|
|
93
|
-
expect(headers['X-Frame-Options']).toBe('DENY');
|
|
94
|
-
expect(headers['X-Content-Type-Options']).toBe('nosniff');
|
|
95
|
-
expect(headers['Referrer-Policy']).toBe('no-referrer');
|
|
96
|
-
expect(headers['Cache-Control']).toContain('no-store');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('uses a long-lived immutable cache for the cacheable script bundle', () => {
|
|
100
|
-
const headers = buildHubSecurityHeaders('n1', { cacheable: true });
|
|
101
|
-
expect(headers['Cache-Control']).toContain('max-age');
|
|
102
|
-
expect(headers['Cache-Control']).toContain('immutable');
|
|
103
|
-
expect(headers['Cache-Control']).not.toContain('no-store');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { isHubEnabled, isHubQueriesEnabled, normalizeHubConfig } from './hub-config.helper';
|
|
4
|
-
|
|
5
|
-
describe('hub-config.helper', () => {
|
|
6
|
-
describe('isHubEnabled', () => {
|
|
7
|
-
it('is disabled by default (undefined)', () => {
|
|
8
|
-
expect(isHubEnabled(undefined)).toBe(false);
|
|
9
|
-
expect(isHubEnabled(false)).toBe(false);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('is enabled for true / {} / a config object', () => {
|
|
13
|
-
expect(isHubEnabled(true)).toBe(true);
|
|
14
|
-
expect(isHubEnabled({})).toBe(true);
|
|
15
|
-
expect(isHubEnabled({ path: 'x' })).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('respects explicit { enabled: false }', () => {
|
|
19
|
-
expect(isHubEnabled({ enabled: false, path: 'x' })).toBe(false);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('isHubQueriesEnabled', () => {
|
|
24
|
-
it('is off by default even when the hub is enabled', () => {
|
|
25
|
-
expect(isHubQueriesEnabled(true)).toBe(false);
|
|
26
|
-
expect(isHubQueriesEnabled({})).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('is on when queries collector is explicitly enabled', () => {
|
|
30
|
-
expect(isHubQueriesEnabled({ collectors: { queries: true } })).toBe(true);
|
|
31
|
-
expect(isHubQueriesEnabled({ collectors: { queries: { warnMs: 5 } } })).toBe(true);
|
|
32
|
-
expect(isHubQueriesEnabled({ collectors: { queries: { enabled: false } } })).toBe(false);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('is off when the hub itself is disabled', () => {
|
|
36
|
-
expect(isHubQueriesEnabled(false)).toBe(false);
|
|
37
|
-
expect(isHubQueriesEnabled({ collectors: { queries: true }, enabled: false })).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('normalizeHubConfig', () => {
|
|
42
|
-
it('applies defaults for the boolean-true form', () => {
|
|
43
|
-
const cfg = normalizeHubConfig(true, { env: 'local', version: '1.2.3' });
|
|
44
|
-
|
|
45
|
-
expect(cfg.path).toBe('hub');
|
|
46
|
-
expect(cfg.roles).toEqual(['admin']);
|
|
47
|
-
expect(cfg.actions).toBe(true);
|
|
48
|
-
expect(cfg.pollIntervalMs).toBe(5000);
|
|
49
|
-
expect(cfg.env).toBe('local');
|
|
50
|
-
expect(cfg.version).toBe('1.2.3');
|
|
51
|
-
// Default collectors: logs + traces on, queries off.
|
|
52
|
-
expect(cfg.collectors.logs).not.toBe(false);
|
|
53
|
-
expect(cfg.collectors.traces).not.toBe(false);
|
|
54
|
-
expect(cfg.collectors.queries).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('clamps the poll interval to the minimum', () => {
|
|
58
|
-
const cfg = normalizeHubConfig({ pollIntervalMs: 10 }, { env: 'local', version: '1' });
|
|
59
|
-
expect(cfg.pollIntervalMs).toBe(1000);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('normalizes a single role string to an array', () => {
|
|
63
|
-
const cfg = normalizeHubConfig({ roles: 'S_USER' }, { env: 'local', version: '1' });
|
|
64
|
-
expect(cfg.roles).toEqual(['S_USER']);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('keeps roles: false (opt-out of auth)', () => {
|
|
68
|
-
const cfg = normalizeHubConfig({ roles: false }, { env: 'local', version: '1' });
|
|
69
|
-
expect(cfg.roles).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('resolves the queries collector with thresholds when enabled', () => {
|
|
73
|
-
const cfg = normalizeHubConfig({ collectors: { queries: { warnMs: 5 } } }, { env: 'local', version: '1' });
|
|
74
|
-
expect(cfg.collectors.queries).not.toBe(false);
|
|
75
|
-
if (cfg.collectors.queries) {
|
|
76
|
-
expect(cfg.collectors.queries.warnMs).toBe(5);
|
|
77
|
-
expect(cfg.collectors.queries.criticalMs).toBe(200);
|
|
78
|
-
expect(cfg.collectors.queries.capacity).toBe(500);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('defaults the mailbox to disabled but enables it with capture mode when present', () => {
|
|
83
|
-
expect(normalizeHubConfig({}, { env: 'local', version: '1' }).mailbox).toBe(false);
|
|
84
|
-
|
|
85
|
-
const cfg = normalizeHubConfig({ mailbox: true }, { env: 'local', version: '1' });
|
|
86
|
-
expect(cfg.mailbox).not.toBe(false);
|
|
87
|
-
if (cfg.mailbox) {
|
|
88
|
-
expect(cfg.mailbox.mode).toBe('capture');
|
|
89
|
-
expect(cfg.mailbox.capacity).toBe(100);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('resolves default external links', () => {
|
|
94
|
-
const cfg = normalizeHubConfig(
|
|
95
|
-
{},
|
|
96
|
-
{ env: 'local', graphQlEnabled: true, permissionsPath: 'permissions', version: '1' },
|
|
97
|
-
);
|
|
98
|
-
expect(cfg.links.swagger).toBe('/swagger');
|
|
99
|
-
expect(cfg.links.graphql).toBe('/graphql');
|
|
100
|
-
expect(cfg.links.permissions).toBe('/permissions');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('omits the graphql link when GraphQL is disabled', () => {
|
|
104
|
-
const cfg = normalizeHubConfig({}, { env: 'local', graphQlEnabled: false, version: '1' });
|
|
105
|
-
expect(cfg.links.graphql).toBeUndefined();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { HubRingBuffer } from './hub-ring-buffer';
|
|
4
|
-
|
|
5
|
-
interface TestEntry {
|
|
6
|
-
seq: number;
|
|
7
|
-
timestamp: number;
|
|
8
|
-
value: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('HubRingBuffer', () => {
|
|
12
|
-
it('assigns monotonic sequence numbers and a timestamp', () => {
|
|
13
|
-
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
14
|
-
const a = buffer.add({ value: 'a' });
|
|
15
|
-
const b = buffer.add({ value: 'b' });
|
|
16
|
-
|
|
17
|
-
expect(a.seq).toBe(0);
|
|
18
|
-
expect(b.seq).toBe(1);
|
|
19
|
-
expect(typeof a.timestamp).toBe('number');
|
|
20
|
-
expect(b.seq).toBeGreaterThan(a.seq);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('honors an explicitly provided timestamp', () => {
|
|
24
|
-
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
25
|
-
const entry = buffer.add({ timestamp: 123456, value: 'a' });
|
|
26
|
-
|
|
27
|
-
expect(entry.timestamp).toBe(123456);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('evicts the oldest entries once capacity is exceeded (fixed memory)', () => {
|
|
31
|
-
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
32
|
-
for (const value of ['a', 'b', 'c', 'd', 'e']) {
|
|
33
|
-
buffer.add({ value });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const items = buffer.recent();
|
|
37
|
-
expect(items.map((i) => i.value)).toEqual(['c', 'd', 'e']);
|
|
38
|
-
expect(buffer.size).toBe(3);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('recent(limit) returns the newest N entries, oldest→newest', () => {
|
|
42
|
-
const buffer = new HubRingBuffer<TestEntry>(5);
|
|
43
|
-
for (const value of ['a', 'b', 'c', 'd']) {
|
|
44
|
-
buffer.add({ value });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
expect(buffer.recent(2).map((i) => i.value)).toEqual(['c', 'd']);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('since(seq) returns only entries newer than the cursor', () => {
|
|
51
|
-
const buffer = new HubRingBuffer<TestEntry>(10);
|
|
52
|
-
buffer.add({ value: 'a' }); // seq 0
|
|
53
|
-
const b = buffer.add({ value: 'b' }); // seq 1
|
|
54
|
-
buffer.add({ value: 'c' }); // seq 2
|
|
55
|
-
|
|
56
|
-
expect(buffer.since(b.seq).map((i) => i.value)).toEqual(['c']);
|
|
57
|
-
expect(buffer.since(-1).map((i) => i.value)).toEqual(['a', 'b', 'c']);
|
|
58
|
-
expect(buffer.since(buffer.lastSeq)).toEqual([]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('since(seq) clamps to what is still retained after eviction', () => {
|
|
62
|
-
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
63
|
-
for (const value of ['a', 'b', 'c', 'd', 'e']) {
|
|
64
|
-
buffer.add({ value }); // seqs 0..4, only 2,3,4 retained
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Asking since an evicted cursor (seq 0) returns only what survives, no crash, no duplicates.
|
|
68
|
-
expect(buffer.since(0).map((i) => i.value)).toEqual(['c', 'd', 'e']);
|
|
69
|
-
expect(buffer.firstRetainedSeq).toBe(2);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('exposes lastSeq (-1 when empty) and firstRetainedSeq', () => {
|
|
73
|
-
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
74
|
-
expect(buffer.lastSeq).toBe(-1);
|
|
75
|
-
expect(buffer.firstRetainedSeq).toBe(-1);
|
|
76
|
-
expect(buffer.size).toBe(0);
|
|
77
|
-
|
|
78
|
-
buffer.add({ value: 'a' });
|
|
79
|
-
expect(buffer.lastSeq).toBe(0);
|
|
80
|
-
expect(buffer.firstRetainedSeq).toBe(0);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('clear() empties the buffer but keeps the sequence counter monotonic', () => {
|
|
84
|
-
const buffer = new HubRingBuffer<TestEntry>(3);
|
|
85
|
-
buffer.add({ value: 'a' });
|
|
86
|
-
buffer.add({ value: 'b' });
|
|
87
|
-
buffer.clear();
|
|
88
|
-
|
|
89
|
-
expect(buffer.size).toBe(0);
|
|
90
|
-
expect(buffer.recent()).toEqual([]);
|
|
91
|
-
// A new entry after clear must not reuse an old seq (cursors stay valid across a clear).
|
|
92
|
-
const c = buffer.add({ value: 'c' });
|
|
93
|
-
expect(c.seq).toBe(2);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { CoreHubMailboxService } from './core-hub-mailbox.service';
|
|
4
|
-
import { ResolvedHubConfig } from '../interfaces/hub-config.interface';
|
|
5
|
-
|
|
6
|
-
function makeConfig(overrides: Partial<ResolvedHubConfig> = {}): ResolvedHubConfig {
|
|
7
|
-
return {
|
|
8
|
-
actions: true,
|
|
9
|
-
allowPublicAccessInProduction: false,
|
|
10
|
-
collectors: { logs: false, queries: false, traces: false },
|
|
11
|
-
db: false,
|
|
12
|
-
emailPreview: true,
|
|
13
|
-
env: 'local',
|
|
14
|
-
links: {},
|
|
15
|
-
loginEndpoint: '/iam/sign-in/email',
|
|
16
|
-
logoutEndpoint: '/iam/sign-out',
|
|
17
|
-
mailbox: { capacity: 3, maxMailSize: 100, mode: 'capture' },
|
|
18
|
-
migrations: false,
|
|
19
|
-
path: 'hub',
|
|
20
|
-
pollIntervalMs: 5000,
|
|
21
|
-
roles: ['admin'],
|
|
22
|
-
version: '1.0.0',
|
|
23
|
-
...overrides,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('CoreHubMailboxService', () => {
|
|
28
|
-
it('captures a mail and returns true to skip transport in capture mode', () => {
|
|
29
|
-
const service = new CoreHubMailboxService(makeConfig());
|
|
30
|
-
const skip = service.capture({ html: '<b>hi</b>', subject: 'Hello', to: 'a@b.com' });
|
|
31
|
-
|
|
32
|
-
expect(skip).toBe(true);
|
|
33
|
-
const data = service.getMailbox();
|
|
34
|
-
expect(data.mode).toBe('capture');
|
|
35
|
-
expect(data.mails).toHaveLength(1);
|
|
36
|
-
expect(data.mails[0].subject).toBe('Hello');
|
|
37
|
-
expect(data.mails[0].to).toBe('a@b.com');
|
|
38
|
-
expect(data.mails[0].hasHtml).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('returns false in copy mode (mail is still sent)', () => {
|
|
42
|
-
const service = new CoreHubMailboxService(makeConfig({ mailbox: { capacity: 3, maxMailSize: 100, mode: 'copy' } }));
|
|
43
|
-
const skip = service.capture({ subject: 'Copy', to: 'a@b.com' });
|
|
44
|
-
|
|
45
|
-
expect(skip).toBe(false);
|
|
46
|
-
expect(service.getMailbox().mails).toHaveLength(1);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('redacts tokens from the stored body in COPY mode (the real mail was still delivered)', () => {
|
|
50
|
-
const service = new CoreHubMailboxService(
|
|
51
|
-
makeConfig({ mailbox: { capacity: 3, maxMailSize: 5000, mode: 'copy' } }),
|
|
52
|
-
);
|
|
53
|
-
const seq = service.captureAndGetSeq({
|
|
54
|
-
html: '<a href="https://app.example.com/reset?token=SUPERSECRETVALUE123">Reset</a>',
|
|
55
|
-
subject: 'Reset',
|
|
56
|
-
to: 'a@b.com',
|
|
57
|
-
});
|
|
58
|
-
const html = service.getMailHtml(seq);
|
|
59
|
-
expect(html).toBeDefined();
|
|
60
|
-
// The captured audit copy must not expose a replayable reset token.
|
|
61
|
-
expect(html).not.toContain('SUPERSECRETVALUE123');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('keeps the raw body in CAPTURE mode (dev/test needs the clickable link; nothing was sent)', () => {
|
|
65
|
-
const service = new CoreHubMailboxService(makeConfig());
|
|
66
|
-
const seq = service.captureAndGetSeq({
|
|
67
|
-
html: '<a href="https://app.example.com/reset?token=SUPERSECRETVALUE123">Reset</a>',
|
|
68
|
-
subject: 'Reset',
|
|
69
|
-
to: 'a@b.com',
|
|
70
|
-
});
|
|
71
|
-
expect(service.getMailHtml(seq)).toContain('SUPERSECRETVALUE123');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('evicts old mails beyond capacity (ring buffer)', () => {
|
|
75
|
-
const service = new CoreHubMailboxService(makeConfig());
|
|
76
|
-
for (const n of [1, 2, 3, 4, 5]) {
|
|
77
|
-
service.capture({ subject: `Mail ${n}`, to: 'a@b.com' });
|
|
78
|
-
}
|
|
79
|
-
const data = service.getMailbox();
|
|
80
|
-
expect(data.mails).toHaveLength(3);
|
|
81
|
-
expect(data.mails.map((m) => m.subject)).toEqual(['Mail 3', 'Mail 4', 'Mail 5']);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('truncates oversized bodies to the per-mail cap', () => {
|
|
85
|
-
const service = new CoreHubMailboxService(makeConfig());
|
|
86
|
-
const big = 'x'.repeat(500);
|
|
87
|
-
const seq = service.captureAndGetSeq({ html: big, subject: 'Big', to: 'a@b.com' });
|
|
88
|
-
const html = service.getMailHtml(seq);
|
|
89
|
-
expect(html).toBeDefined();
|
|
90
|
-
expect((html as string).length).toBeLessThanOrEqual(100 + 40); // cap + truncation marker
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('serves stored HTML by seq for the preview iframe', () => {
|
|
94
|
-
const service = new CoreHubMailboxService(makeConfig());
|
|
95
|
-
const seq = service.captureAndGetSeq({ html: '<p>Body</p>', subject: 'S', to: 'a@b.com' });
|
|
96
|
-
expect(service.getMailHtml(seq)).toContain('<p>Body</p>');
|
|
97
|
-
expect(service.getMailHtml(9999)).toBeUndefined();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('clear() empties the mailbox', () => {
|
|
101
|
-
const service = new CoreHubMailboxService(makeConfig());
|
|
102
|
-
service.capture({ subject: 'S', to: 'a@b.com' });
|
|
103
|
-
service.clear();
|
|
104
|
-
expect(service.getMailbox().mails).toHaveLength(0);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('supports cursor-based polling via since', () => {
|
|
108
|
-
const service = new CoreHubMailboxService(makeConfig());
|
|
109
|
-
service.capture({ subject: 'A', to: 'a@b.com' });
|
|
110
|
-
const first = service.getMailbox();
|
|
111
|
-
service.capture({ subject: 'B', to: 'a@b.com' });
|
|
112
|
-
const next = service.getMailbox(first.cursor);
|
|
113
|
-
expect(next.mails).toHaveLength(1);
|
|
114
|
-
expect(next.mails[0].subject).toBe('B');
|
|
115
|
-
});
|
|
116
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { HubTraceBufferService } from './hub-trace-buffer.service';
|
|
4
|
-
import { ResolvedHubConfig } from '../interfaces/hub-config.interface';
|
|
5
|
-
|
|
6
|
-
function makeConfig(overrides: Partial<ResolvedHubConfig> = {}): ResolvedHubConfig {
|
|
7
|
-
return {
|
|
8
|
-
actions: true,
|
|
9
|
-
allowPublicAccessInProduction: false,
|
|
10
|
-
collectors: {
|
|
11
|
-
logs: false,
|
|
12
|
-
queries: false,
|
|
13
|
-
traces: { capacity: 50, captureGraphQlOperation: true, excludePaths: [], slowMs: 1000 },
|
|
14
|
-
},
|
|
15
|
-
db: false,
|
|
16
|
-
emailPreview: true,
|
|
17
|
-
env: 'local',
|
|
18
|
-
links: {},
|
|
19
|
-
loginEndpoint: '/iam/sign-in/email',
|
|
20
|
-
logoutEndpoint: '/iam/sign-out',
|
|
21
|
-
mailbox: false,
|
|
22
|
-
migrations: false,
|
|
23
|
-
path: 'hub',
|
|
24
|
-
pollIntervalMs: 5000,
|
|
25
|
-
roles: ['admin'],
|
|
26
|
-
version: '1.0.0',
|
|
27
|
-
...overrides,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Minimal Express req/res doubles for record(). */
|
|
32
|
-
function reqRes(path: string, method = 'GET', statusCode = 200) {
|
|
33
|
-
return {
|
|
34
|
-
req: { baseUrl: '', method, path, route: undefined } as never,
|
|
35
|
-
res: { getHeader: () => undefined, statusCode, writableFinished: true } as never,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe('HubTraceBufferService', () => {
|
|
40
|
-
describe('isExcluded (boundary-aware)', () => {
|
|
41
|
-
it('excludes the Hub base path and its sub-paths', () => {
|
|
42
|
-
const service = new HubTraceBufferService(makeConfig());
|
|
43
|
-
expect(service.isExcluded('/hub')).toBe(true);
|
|
44
|
-
expect(service.isExcluded('/hub/logs.json')).toBe(true);
|
|
45
|
-
expect(service.isExcluded('/hub/actions/collectors/traces/clear')).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('does NOT exclude a sibling route that merely shares the prefix string', () => {
|
|
49
|
-
const service = new HubTraceBufferService(makeConfig());
|
|
50
|
-
// Regression guard for the over-broad `startsWith(prefix)` bug: `/hubble` is not a Hub route.
|
|
51
|
-
expect(service.isExcluded('/hubble')).toBe(false);
|
|
52
|
-
expect(service.isExcluded('/hub-admin')).toBe(false);
|
|
53
|
-
expect(service.isExcluded('/hubbard/x')).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('honours additional configured exclude prefixes with the same boundary rule', () => {
|
|
57
|
-
const service = new HubTraceBufferService(
|
|
58
|
-
makeConfig({
|
|
59
|
-
collectors: {
|
|
60
|
-
logs: false,
|
|
61
|
-
queries: false,
|
|
62
|
-
traces: { capacity: 50, captureGraphQlOperation: true, excludePaths: ['/health'], slowMs: 1000 },
|
|
63
|
-
},
|
|
64
|
-
}),
|
|
65
|
-
);
|
|
66
|
-
expect(service.isExcluded('/health')).toBe(true);
|
|
67
|
-
expect(service.isExcluded('/health/live')).toBe(true);
|
|
68
|
-
expect(service.isExcluded('/healthcheck')).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('record + getData', () => {
|
|
73
|
-
it('records a completed request with method, path and duration, and summarises', () => {
|
|
74
|
-
const service = new HubTraceBufferService(makeConfig());
|
|
75
|
-
const { req, res } = reqRes('/permissions/json');
|
|
76
|
-
service.record(req, res, 12.5, false, '/permissions/json');
|
|
77
|
-
|
|
78
|
-
const data = service.getData();
|
|
79
|
-
expect(data.traces).toHaveLength(1);
|
|
80
|
-
expect(data.traces[0].method).toBe('GET');
|
|
81
|
-
expect(data.traces[0].path).toBe('/permissions/json');
|
|
82
|
-
expect(data.traces[0].durationMs).toBe(12.5);
|
|
83
|
-
expect(data.summary.total).toBe(1);
|
|
84
|
-
expect(data.summary.avgMs).toBe(12.5);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('flags slow and error traces in the summary', () => {
|
|
88
|
-
const service = new HubTraceBufferService(makeConfig());
|
|
89
|
-
const slow = reqRes('/slow');
|
|
90
|
-
service.record(slow.req, slow.res, 5000, false, '/slow');
|
|
91
|
-
const err = reqRes('/boom', 'GET', 500);
|
|
92
|
-
service.record(err.req, err.res, 3, false, '/boom');
|
|
93
|
-
|
|
94
|
-
const data = service.getData();
|
|
95
|
-
expect(data.summary.slowCount).toBe(1);
|
|
96
|
-
expect(data.summary.errorCount).toBe(1);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('supports cursor-based polling via since', () => {
|
|
100
|
-
const service = new HubTraceBufferService(makeConfig());
|
|
101
|
-
const a = reqRes('/a');
|
|
102
|
-
service.record(a.req, a.res, 1, false, '/a');
|
|
103
|
-
const cursor = service.getData().cursor;
|
|
104
|
-
const b = reqRes('/b');
|
|
105
|
-
service.record(b.req, b.res, 2, false, '/b');
|
|
106
|
-
|
|
107
|
-
const next = service.getData(cursor);
|
|
108
|
-
expect(next.traces).toHaveLength(1);
|
|
109
|
-
expect(next.traces[0].path).toBe('/b');
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
});
|