@open-mercato/shared 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +57 -6
- package/build.mjs +2 -4
- package/dist/lib/ai/safety-identifier.js +32 -0
- package/dist/lib/ai/safety-identifier.js.map +7 -0
- package/dist/lib/auth/organizationScope.js +34 -0
- package/dist/lib/auth/organizationScope.js.map +7 -0
- package/dist/lib/auth/server.js +15 -2
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
- package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +117 -43
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +5 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +2 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +2 -11
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +72 -9
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-runner.js +2 -2
- package/dist/lib/crud/interceptor-runner.js.map +2 -2
- package/dist/lib/crud/mutation-guard-registry.js +2 -2
- package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
- package/dist/lib/crud/types.js +4 -0
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/consistency.js +19 -0
- package/dist/lib/data/consistency.js.map +7 -0
- package/dist/lib/data/engine.js +37 -11
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/entityMetadata.js +26 -0
- package/dist/lib/db/entityMetadata.js.map +7 -0
- package/dist/lib/db/pg-errors.js +42 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +48 -6
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +56 -0
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +2 -10
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/extension.js +29 -0
- package/dist/lib/logger/extension.js.map +7 -0
- package/dist/lib/logger/index.js +47 -3
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +5 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/modules/surfaceFingerprint.js +47 -0
- package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
- package/dist/lib/query/ciphertext-search-warning.js +45 -0
- package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
- package/dist/lib/query/engine.js +54 -40
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +7 -2
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/auto-indexing.js +14 -0
- package/dist/lib/search/auto-indexing.js.map +7 -0
- package/dist/lib/search/availability.js +107 -0
- package/dist/lib/search/availability.js.map +7 -0
- package/dist/lib/search/config.js +61 -2
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenLookup.js +46 -0
- package/dist/lib/search/tokenLookup.js.map +7 -0
- package/dist/lib/search/tokenize.js +23 -11
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/telemetry/runtime.js +35 -0
- package/dist/lib/telemetry/runtime.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +57 -0
- package/dist/lib/webhooks/body.js.map +7 -0
- package/dist/lib/webhooks/inbound-types.js +1 -0
- package/dist/lib/webhooks/inbound-types.js.map +7 -0
- package/dist/lib/webhooks/index.js +13 -1
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +12 -7
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/overrides.js +50 -1
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +8 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js +1 -0
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/extension-points.js +96 -0
- package/dist/modules/widgets/extension-points.js.map +7 -0
- package/dist/security/enabledModulesRegistry.js +56 -11
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/dist/security/featurePolicy.js +62 -0
- package/dist/security/featurePolicy.js.map +7 -0
- package/jest.config.cjs +2 -1
- package/package.json +19 -9
- package/scripts/versionSource.cjs +65 -0
- package/src/lib/__tests__/versionSource.test.ts +46 -0
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
- package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
- package/src/lib/ai/llm-provider.ts +33 -0
- package/src/lib/ai/safety-identifier.ts +73 -0
- package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
- package/src/lib/auth/__tests__/server.test.ts +23 -0
- package/src/lib/auth/organizationScope.ts +65 -0
- package/src/lib/auth/server.ts +23 -2
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
- package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +213 -65
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
- package/src/lib/commands/command-bus.ts +5 -1
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +351 -0
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
- package/src/lib/crud/enricher-runner.ts +2 -11
- package/src/lib/crud/factory.ts +82 -6
- package/src/lib/crud/interceptor-runner.ts +2 -2
- package/src/lib/crud/mutation-guard-registry.ts +2 -2
- package/src/lib/crud/types.ts +3 -0
- package/src/lib/data/__tests__/consistency.test.ts +38 -0
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
- package/src/lib/data/consistency.ts +17 -0
- package/src/lib/data/engine.ts +50 -16
- package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
- package/src/lib/db/entityMetadata.ts +43 -0
- package/src/lib/db/pg-errors.ts +58 -0
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
- package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
- package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
- package/src/lib/di/container.ts +72 -8
- package/src/lib/email/__tests__/send.test.ts +37 -1
- package/src/lib/email/send.ts +79 -0
- package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
- package/src/lib/encryption/subscriber.ts +2 -14
- package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
- package/src/lib/i18n/__tests__/config.test.ts +8 -0
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +6 -2
- package/src/lib/logger/__tests__/logger.test.ts +77 -0
- package/src/lib/logger/extension.ts +59 -0
- package/src/lib/logger/index.ts +59 -1
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
- package/src/lib/modules/registry.ts +10 -0
- package/src/lib/modules/surfaceFingerprint.ts +87 -0
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
- package/src/lib/query/__tests__/engine.test.ts +65 -13
- package/src/lib/query/ciphertext-search-warning.ts +95 -0
- package/src/lib/query/engine.ts +75 -58
- package/src/lib/query/types.ts +4 -5
- package/src/lib/redis/__tests__/connection.test.ts +16 -0
- package/src/lib/redis/connection.ts +10 -7
- package/src/lib/search/__tests__/availability.test.ts +211 -0
- package/src/lib/search/__tests__/config.test.ts +177 -0
- package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
- package/src/lib/search/__tests__/tokenize.test.ts +49 -0
- package/src/lib/search/auto-indexing.ts +22 -0
- package/src/lib/search/availability.ts +232 -0
- package/src/lib/search/config.ts +106 -8
- package/src/lib/search/tokenLookup.ts +133 -0
- package/src/lib/search/tokenize.ts +33 -11
- package/src/lib/telemetry/runtime.ts +72 -0
- package/src/lib/webhooks/__tests__/body.test.ts +95 -0
- package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
- package/src/lib/webhooks/body.ts +70 -0
- package/src/lib/webhooks/inbound-types.ts +125 -0
- package/src/lib/webhooks/index.ts +19 -1
- package/src/lib/webhooks/verify.ts +17 -9
- package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
- package/src/modules/__tests__/registry.test.ts +10 -2
- package/src/modules/analytics.ts +7 -0
- package/src/modules/customer-auth.ts +1 -0
- package/src/modules/encryption.ts +3 -0
- package/src/modules/navigation/backendChrome.ts +15 -0
- package/src/modules/overrides.ts +103 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
- package/src/modules/payment_gateways/types.ts +41 -0
- package/src/modules/registry.ts +3 -0
- package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
- package/src/modules/widgets/extension-points.ts +524 -0
- package/src/security/__tests__/featurePolicy.test.ts +166 -0
- package/src/security/enabledModulesRegistry.ts +64 -12
- package/src/security/featurePolicy.ts +89 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Tests for the app-level DI override hook (`src/di.ts` → `@/di`) in
|
|
2
|
+
// `createRequestContainer()`. The hook used to swallow EVERY failure with
|
|
3
|
+
// bare `catch {}` blocks, so a src/di.ts whose register() threw — or a @/di
|
|
4
|
+
// module that failed to load for any reason other than being absent — was
|
|
5
|
+
// skipped without a trace and the documented override point silently did
|
|
6
|
+
// nothing. Absence of the optional module must stay quiet; real failures
|
|
7
|
+
// must be logged.
|
|
8
|
+
|
|
9
|
+
import { asValue } from 'awilix'
|
|
10
|
+
|
|
11
|
+
jest.mock(
|
|
12
|
+
'@open-mercato/shared/lib/db/mikro',
|
|
13
|
+
() => {
|
|
14
|
+
const baseEm: any = {
|
|
15
|
+
fork: () => baseEm,
|
|
16
|
+
getRepository: () => ({ find: async () => [], findOne: async () => null }),
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
__esModule: true,
|
|
20
|
+
getOrm: async () => ({ em: baseEm }),
|
|
21
|
+
getOrmEntities: () => [],
|
|
22
|
+
registerOrmEntities: () => {},
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{ virtual: false },
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
jest.mock(
|
|
29
|
+
'@mikro-orm/core',
|
|
30
|
+
() => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
RequestContext: { getEntityManager: () => null },
|
|
33
|
+
}),
|
|
34
|
+
{ virtual: true },
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
jest.mock(
|
|
38
|
+
'@open-mercato/shared/lib/query/engine',
|
|
39
|
+
() => ({ __esModule: true, BasicQueryEngine: class {} }),
|
|
40
|
+
{ virtual: false },
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
jest.mock(
|
|
44
|
+
'@open-mercato/shared/lib/data/engine',
|
|
45
|
+
() => ({ __esModule: true, DefaultDataEngine: class {} }),
|
|
46
|
+
{ virtual: false },
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
jest.mock(
|
|
50
|
+
'@open-mercato/shared/lib/commands',
|
|
51
|
+
() => ({
|
|
52
|
+
__esModule: true,
|
|
53
|
+
commandRegistry: {},
|
|
54
|
+
CommandBus: class { constructor() {} },
|
|
55
|
+
}),
|
|
56
|
+
{ virtual: false },
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
jest.mock(
|
|
60
|
+
'@open-mercato/shared/modules/overrides',
|
|
61
|
+
() => ({
|
|
62
|
+
__esModule: true,
|
|
63
|
+
applyDiOverridesToContainer: () => {},
|
|
64
|
+
}),
|
|
65
|
+
{ virtual: false },
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Keep this suite hermetic: without this mock the REAL core bootstrap module
|
|
69
|
+
// loads (and runs its module-level side effects) whenever generated files
|
|
70
|
+
// exist on disk, polluting globalThis state shared with sibling test files.
|
|
71
|
+
jest.mock(
|
|
72
|
+
'@open-mercato/core/bootstrap',
|
|
73
|
+
() => ({
|
|
74
|
+
__esModule: true,
|
|
75
|
+
bootstrap: async () => {},
|
|
76
|
+
}),
|
|
77
|
+
{ virtual: true },
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
jest.mock(
|
|
81
|
+
'@open-mercato/shared/lib/encryption/subscriber',
|
|
82
|
+
() => ({
|
|
83
|
+
__esModule: true,
|
|
84
|
+
registerTenantEncryptionSubscriber: () => {},
|
|
85
|
+
}),
|
|
86
|
+
{ virtual: true },
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const mockWarn = jest.fn()
|
|
90
|
+
jest.mock('../../logger', () => ({
|
|
91
|
+
createLogger: () => ({
|
|
92
|
+
debug: jest.fn(),
|
|
93
|
+
info: jest.fn(),
|
|
94
|
+
warn: (...args: unknown[]) => mockWarn(...args),
|
|
95
|
+
error: jest.fn(),
|
|
96
|
+
child() { return this },
|
|
97
|
+
}),
|
|
98
|
+
}))
|
|
99
|
+
|
|
100
|
+
const mockAppDiRegister = jest.fn()
|
|
101
|
+
jest.mock(
|
|
102
|
+
'@/di',
|
|
103
|
+
() => ({
|
|
104
|
+
__esModule: true,
|
|
105
|
+
register: (...args: unknown[]) => mockAppDiRegister(...args),
|
|
106
|
+
}),
|
|
107
|
+
{ virtual: true },
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const {
|
|
111
|
+
createRequestContainer,
|
|
112
|
+
registerAppDiRegistrar,
|
|
113
|
+
registerDiRegistrars,
|
|
114
|
+
resetBootstrapCache,
|
|
115
|
+
} = require('@open-mercato/shared/lib/di/container')
|
|
116
|
+
|
|
117
|
+
describe('app-level DI override hook (@/di)', () => {
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
resetBootstrapCache()
|
|
120
|
+
registerAppDiRegistrar(null)
|
|
121
|
+
registerDiRegistrars([])
|
|
122
|
+
mockWarn.mockClear()
|
|
123
|
+
mockAppDiRegister.mockReset()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('calls register() with the request container and applies its registrations', async () => {
|
|
127
|
+
mockAppDiRegister.mockImplementation((container: any) => {
|
|
128
|
+
container.register({ appOverrideProbe: asValue('from-app-di') })
|
|
129
|
+
})
|
|
130
|
+
const container = await createRequestContainer()
|
|
131
|
+
expect(mockAppDiRegister).toHaveBeenCalledTimes(1)
|
|
132
|
+
expect(mockAppDiRegister).toHaveBeenCalledWith(container)
|
|
133
|
+
expect(container.resolve('appOverrideProbe')).toBe('from-app-di')
|
|
134
|
+
expect(mockWarn).not.toHaveBeenCalled()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('awaits an async register()', async () => {
|
|
138
|
+
mockAppDiRegister.mockImplementation(async (container: any) => {
|
|
139
|
+
await Promise.resolve()
|
|
140
|
+
container.register({ appOverrideProbe: asValue('async-app-di') })
|
|
141
|
+
})
|
|
142
|
+
const container = await createRequestContainer()
|
|
143
|
+
expect(container.resolve('appOverrideProbe')).toBe('async-app-di')
|
|
144
|
+
expect(mockWarn).not.toHaveBeenCalled()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('warns instead of silently swallowing when register() throws', async () => {
|
|
148
|
+
mockAppDiRegister.mockImplementation(() => {
|
|
149
|
+
throw new Error('boom from app di')
|
|
150
|
+
})
|
|
151
|
+
const firstContainer = await createRequestContainer()
|
|
152
|
+
const secondContainer = await createRequestContainer()
|
|
153
|
+
expect(firstContainer).toBeDefined()
|
|
154
|
+
expect(secondContainer).toBeDefined()
|
|
155
|
+
expect(mockAppDiRegister).toHaveBeenCalledTimes(2)
|
|
156
|
+
expect(mockWarn).toHaveBeenCalledTimes(1)
|
|
157
|
+
const [message, fields] = mockWarn.mock.calls[0]
|
|
158
|
+
expect(String(message)).toContain('register()')
|
|
159
|
+
expect((fields as { err: Error }).err.message).toBe('boom from app di')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('warns when an async register() rejects', async () => {
|
|
163
|
+
mockAppDiRegister.mockImplementation(async () => {
|
|
164
|
+
throw new Error('async boom from app di')
|
|
165
|
+
})
|
|
166
|
+
const firstContainer = await createRequestContainer()
|
|
167
|
+
const secondContainer = await createRequestContainer()
|
|
168
|
+
expect(firstContainer).toBeDefined()
|
|
169
|
+
expect(secondContainer).toBeDefined()
|
|
170
|
+
expect(mockAppDiRegister).toHaveBeenCalledTimes(2)
|
|
171
|
+
expect(mockWarn).toHaveBeenCalledTimes(1)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// A module DI registrar that throws is skipped fail-open (one broken module
|
|
2
|
+
// must not take down every request container), but the failure MUST be logged —
|
|
3
|
+
// a silent catch leaves the module's services missing with no trace until an
|
|
4
|
+
// unrelated Awilix resolution error surfaces much later.
|
|
5
|
+
|
|
6
|
+
import { asValue } from 'awilix'
|
|
7
|
+
import type { AwilixContainer } from 'awilix'
|
|
8
|
+
|
|
9
|
+
const mockLoggerError = jest.fn()
|
|
10
|
+
|
|
11
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
12
|
+
const makeLogger = (): Record<string, unknown> => ({
|
|
13
|
+
error: (...args: unknown[]) => mockLoggerError(...args),
|
|
14
|
+
warn: jest.fn(),
|
|
15
|
+
info: jest.fn(),
|
|
16
|
+
debug: jest.fn(),
|
|
17
|
+
trace: jest.fn(),
|
|
18
|
+
child: () => makeLogger(),
|
|
19
|
+
})
|
|
20
|
+
return {
|
|
21
|
+
__esModule: true,
|
|
22
|
+
createLogger: () => makeLogger(),
|
|
23
|
+
getLogLevel: () => 'info',
|
|
24
|
+
isLevelEnabled: () => false,
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
jest.mock(
|
|
29
|
+
'@open-mercato/shared/lib/db/mikro',
|
|
30
|
+
() => {
|
|
31
|
+
const baseEm: any = {
|
|
32
|
+
fork: () => baseEm,
|
|
33
|
+
getRepository: () => ({ find: async () => [], findOne: async () => null }),
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
__esModule: true,
|
|
37
|
+
getOrm: async () => ({ em: baseEm }),
|
|
38
|
+
getOrmEntities: () => [],
|
|
39
|
+
registerOrmEntities: () => {},
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{ virtual: false },
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
jest.mock(
|
|
46
|
+
'@mikro-orm/core',
|
|
47
|
+
() => ({
|
|
48
|
+
__esModule: true,
|
|
49
|
+
RequestContext: { getEntityManager: () => null },
|
|
50
|
+
}),
|
|
51
|
+
{ virtual: true },
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
jest.mock(
|
|
55
|
+
'@open-mercato/shared/lib/query/engine',
|
|
56
|
+
() => ({ __esModule: true, BasicQueryEngine: class {} }),
|
|
57
|
+
{ virtual: false },
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
jest.mock(
|
|
61
|
+
'@open-mercato/shared/lib/data/engine',
|
|
62
|
+
() => ({ __esModule: true, DefaultDataEngine: class {} }),
|
|
63
|
+
{ virtual: false },
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
jest.mock(
|
|
67
|
+
'@open-mercato/shared/lib/commands',
|
|
68
|
+
() => ({
|
|
69
|
+
__esModule: true,
|
|
70
|
+
commandRegistry: {},
|
|
71
|
+
CommandBus: class { constructor() {} },
|
|
72
|
+
}),
|
|
73
|
+
{ virtual: false },
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
jest.mock(
|
|
77
|
+
'@open-mercato/shared/modules/overrides',
|
|
78
|
+
() => ({
|
|
79
|
+
__esModule: true,
|
|
80
|
+
applyDiOverridesToContainer: () => {},
|
|
81
|
+
applyModuleOverridesToModules: (modules: unknown[]) => modules,
|
|
82
|
+
applyComponentOverridesToEntries: (entries: unknown[]) => entries,
|
|
83
|
+
}),
|
|
84
|
+
{ virtual: false },
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
jest.mock(
|
|
88
|
+
'@open-mercato/core/bootstrap',
|
|
89
|
+
() => ({
|
|
90
|
+
__esModule: true,
|
|
91
|
+
bootstrap: async () => {},
|
|
92
|
+
}),
|
|
93
|
+
{ virtual: true },
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
jest.mock(
|
|
97
|
+
'@open-mercato/shared/lib/encryption/subscriber',
|
|
98
|
+
() => ({
|
|
99
|
+
__esModule: true,
|
|
100
|
+
registerTenantEncryptionSubscriber: () => {},
|
|
101
|
+
}),
|
|
102
|
+
{ virtual: true },
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const {
|
|
106
|
+
createRequestContainer,
|
|
107
|
+
registerAppDiRegistrar,
|
|
108
|
+
registerDiRegistrars,
|
|
109
|
+
resetBootstrapCache,
|
|
110
|
+
} = require('@open-mercato/shared/lib/di/container')
|
|
111
|
+
|
|
112
|
+
describe('createRequestContainer — module DI registrar failures', () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
resetBootstrapCache()
|
|
115
|
+
registerAppDiRegistrar(null)
|
|
116
|
+
mockLoggerError.mockClear()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
afterAll(() => {
|
|
120
|
+
registerDiRegistrars([])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('logs the error and continues with the remaining registrars', async () => {
|
|
124
|
+
const boom = new Error('di exploded')
|
|
125
|
+
const workingRegistrar = jest.fn((container: AwilixContainer) => {
|
|
126
|
+
container.register({ registrarProbeService: asValue('registered') })
|
|
127
|
+
})
|
|
128
|
+
registerDiRegistrars([
|
|
129
|
+
() => { throw boom },
|
|
130
|
+
workingRegistrar,
|
|
131
|
+
])
|
|
132
|
+
|
|
133
|
+
const container = await createRequestContainer()
|
|
134
|
+
|
|
135
|
+
expect(workingRegistrar).toHaveBeenCalledTimes(1)
|
|
136
|
+
expect(container.resolve('registrarProbeService')).toBe('registered')
|
|
137
|
+
expect(mockLoggerError).toHaveBeenCalledWith('Module DI registrar failed', {
|
|
138
|
+
registrarIndex: 0,
|
|
139
|
+
err: boom,
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('does not log when every registrar succeeds', async () => {
|
|
144
|
+
registerDiRegistrars([
|
|
145
|
+
(container: AwilixContainer) => {
|
|
146
|
+
container.register({ registrarProbeService: asValue('ok') })
|
|
147
|
+
},
|
|
148
|
+
])
|
|
149
|
+
|
|
150
|
+
await createRequestContainer()
|
|
151
|
+
|
|
152
|
+
expect(mockLoggerError).not.toHaveBeenCalled()
|
|
153
|
+
})
|
|
154
|
+
})
|
package/src/lib/di/container.ts
CHANGED
|
@@ -24,6 +24,13 @@ export type AppDiRegistrar = (container: AppContainer) => void | Promise<void>
|
|
|
24
24
|
// file can be loaded as multiple module instances when mixing dynamic and static imports
|
|
25
25
|
const GLOBAL_KEY = '__openMercatoDiRegistrars__'
|
|
26
26
|
const APP_DI_REGISTRAR_KEY = '__openMercatoAppDiRegistrar__'
|
|
27
|
+
const APP_DI_LOAD_WARNING_KEY = '__openMercatoAppDiLoadWarningEmitted__'
|
|
28
|
+
const APP_DI_REGISTER_WARNING_KEY = '__openMercatoAppDiRegisterWarningEmitted__'
|
|
29
|
+
// Set once the legacy `@/di` fallback proves the specifier is unresolvable in this process.
|
|
30
|
+
// Worker/CLI processes run against built package output where the app's `@/` alias does not
|
|
31
|
+
// exist, so retrying the import per request container only repeats a failed module resolution
|
|
32
|
+
// (and its log line) once per job.
|
|
33
|
+
const APP_DI_MODULE_UNRESOLVABLE_KEY = '__openMercatoAppDiModuleUnresolvable__'
|
|
27
34
|
// Phase 5 — process-scoped bootstrap cache. The cache/event-bus/encryption
|
|
28
35
|
// services bootstrap() creates are inherently process-scoped (they hold
|
|
29
36
|
// state across requests). Caching them on globalThis after the first
|
|
@@ -118,6 +125,9 @@ export function registerDiRegistrars(registrars: DiRegistrar[]) {
|
|
|
118
125
|
// Force re-bootstrap on HMR — module subscribers may have changed.
|
|
119
126
|
;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = null
|
|
120
127
|
;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined
|
|
128
|
+
// An app that gains a src/di.ts mid-session reloads through here, so the negative
|
|
129
|
+
// resolution result must not outlive the reload.
|
|
130
|
+
;(globalThis as Record<string, unknown>)[APP_DI_MODULE_UNRESOLVABLE_KEY] = undefined
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
export function getDiRegistrars(): DiRegistrar[] {
|
|
@@ -137,10 +147,44 @@ export function registerAppDiRegistrar(registrar: AppDiRegistrar | null): void {
|
|
|
137
147
|
;(globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY] = registrar
|
|
138
148
|
}
|
|
139
149
|
|
|
140
|
-
/** Test-only helper to drop
|
|
150
|
+
/** Test-only helper to drop process-scoped request-container state. */
|
|
141
151
|
export function resetBootstrapCache(): void {
|
|
142
152
|
(globalThis as any)[BOOTSTRAP_CACHE_KEY] = null
|
|
143
153
|
;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined
|
|
154
|
+
;(globalThis as Record<string, unknown>)[APP_DI_LOAD_WARNING_KEY] = undefined
|
|
155
|
+
;(globalThis as Record<string, unknown>)[APP_DI_REGISTER_WARNING_KEY] = undefined
|
|
156
|
+
;(globalThis as Record<string, unknown>)[APP_DI_MODULE_UNRESOLVABLE_KEY] = undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isAppDiModuleUnresolvable(): boolean {
|
|
160
|
+
return (globalThis as Record<string, unknown>)[APP_DI_MODULE_UNRESOLVABLE_KEY] === true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function markAppDiModuleUnresolvable(): void {
|
|
164
|
+
;(globalThis as Record<string, unknown>)[APP_DI_MODULE_UNRESOLVABLE_KEY] = true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isAppDiModuleNotFound(error: unknown): boolean {
|
|
168
|
+
if (!error || typeof error !== 'object') return false
|
|
169
|
+
const { code, message } = error as { code?: unknown; message?: unknown }
|
|
170
|
+
const text = typeof message === 'string' ? message : ''
|
|
171
|
+
const moduleNotFound =
|
|
172
|
+
code === 'MODULE_NOT_FOUND'
|
|
173
|
+
|| code === 'ERR_MODULE_NOT_FOUND'
|
|
174
|
+
|| text.startsWith('Cannot find module')
|
|
175
|
+
|| text.startsWith('Cannot find package')
|
|
176
|
+
return moduleNotFound && /(?:module|package) ['"]@\/di['"]/.test(text)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function warnAppDiFailureOnce(
|
|
180
|
+
key: typeof APP_DI_LOAD_WARNING_KEY | typeof APP_DI_REGISTER_WARNING_KEY,
|
|
181
|
+
message: string,
|
|
182
|
+
error: unknown,
|
|
183
|
+
): void {
|
|
184
|
+
const globalScope = globalThis as Record<string, unknown>
|
|
185
|
+
if (globalScope[key] === true) return
|
|
186
|
+
globalScope[key] = true
|
|
187
|
+
logger.warn(message, { err: error })
|
|
144
188
|
}
|
|
145
189
|
|
|
146
190
|
function isAwilixResolver(value: unknown): value is Resolver<unknown> {
|
|
@@ -197,9 +241,14 @@ export async function createRequestContainer(): Promise<AppContainer> {
|
|
|
197
241
|
createCommandOptimisticLockGuardService(),
|
|
198
242
|
).scoped(),
|
|
199
243
|
})
|
|
200
|
-
// Allow modules to override/extend
|
|
201
|
-
|
|
202
|
-
|
|
244
|
+
// Allow modules to override/extend. Fail-open by design (one broken module's
|
|
245
|
+
// di.ts must not take down every request container), but never silently: the
|
|
246
|
+
// module's services would otherwise vanish with no trace until an unrelated
|
|
247
|
+
// Awilix resolution error surfaces much later.
|
|
248
|
+
for (const [registrarIndex, reg] of diRegistrars.entries()) {
|
|
249
|
+
try { reg?.(container) } catch (error) {
|
|
250
|
+
logger.error('Module DI registrar failed', { registrarIndex, err: error })
|
|
251
|
+
}
|
|
203
252
|
}
|
|
204
253
|
// Core bootstrap (cache, event bus, encryption subscriber/KMS, module subscribers)
|
|
205
254
|
// Phase 5 — process-scoped once-guard. The first request runs the full
|
|
@@ -235,7 +284,7 @@ export async function createRequestContainer(): Promise<AppContainer> {
|
|
|
235
284
|
} catch (error) {
|
|
236
285
|
logger.error('App-level DI registrar failed', { err: error })
|
|
237
286
|
}
|
|
238
|
-
} else {
|
|
287
|
+
} else if (!isAppDiModuleUnresolvable()) {
|
|
239
288
|
// Backward-compatible fallback for apps that have not adopted explicit wiring.
|
|
240
289
|
try {
|
|
241
290
|
// @ts-ignore - @/di only exists in app context, not in packages
|
|
@@ -244,11 +293,26 @@ export async function createRequestContainer(): Promise<AppContainer> {
|
|
|
244
293
|
try {
|
|
245
294
|
const maybe = appDi.register(container)
|
|
246
295
|
if (maybe && typeof maybe.then === 'function') await maybe
|
|
247
|
-
} catch (
|
|
248
|
-
|
|
296
|
+
} catch (err) {
|
|
297
|
+
warnAppDiFailureOnce(
|
|
298
|
+
APP_DI_REGISTER_WARNING_KEY,
|
|
299
|
+
'App-level DI override (src/di.ts register()) threw; its registrations are skipped',
|
|
300
|
+
err,
|
|
301
|
+
)
|
|
249
302
|
}
|
|
250
303
|
}
|
|
251
|
-
} catch {
|
|
304
|
+
} catch (err) {
|
|
305
|
+
if (isAppDiModuleNotFound(err)) {
|
|
306
|
+
markAppDiModuleUnresolvable()
|
|
307
|
+
logger.debug('App-level DI override module (@/di) not resolvable; skipping', { err })
|
|
308
|
+
} else {
|
|
309
|
+
warnAppDiFailureOnce(
|
|
310
|
+
APP_DI_LOAD_WARNING_KEY,
|
|
311
|
+
'App-level DI override module (@/di) failed to load; its registrations are skipped',
|
|
312
|
+
err,
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
252
316
|
}
|
|
253
317
|
applyDiOverridesToContainer({
|
|
254
318
|
register: (registrations) => container.register(toAwilixRegistrations(registrations)),
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
2
5
|
import { sendEmail } from '../send'
|
|
3
6
|
|
|
4
7
|
var sendMock: jest.Mock
|
|
@@ -15,6 +18,7 @@ jest.mock('resend', () => {
|
|
|
15
18
|
|
|
16
19
|
describe('sendEmail', () => {
|
|
17
20
|
const originalEnv = process.env
|
|
21
|
+
let tempDir: string | null = null
|
|
18
22
|
|
|
19
23
|
beforeEach(() => {
|
|
20
24
|
process.env = {
|
|
@@ -26,7 +30,11 @@ describe('sendEmail', () => {
|
|
|
26
30
|
ResendMock.mockClear()
|
|
27
31
|
})
|
|
28
32
|
|
|
29
|
-
afterEach(() => {
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
if (tempDir) {
|
|
35
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
36
|
+
tempDir = null
|
|
37
|
+
}
|
|
30
38
|
process.env = originalEnv
|
|
31
39
|
})
|
|
32
40
|
|
|
@@ -158,4 +166,32 @@ describe('sendEmail', () => {
|
|
|
158
166
|
expect(ResendMock).not.toHaveBeenCalled()
|
|
159
167
|
expect(sendMock).not.toHaveBeenCalled()
|
|
160
168
|
})
|
|
169
|
+
|
|
170
|
+
it('captures email links in OM_TEST_MODE without external delivery', async () => {
|
|
171
|
+
tempDir = await mkdtemp(join(tmpdir(), 'om-email-capture-'))
|
|
172
|
+
const capturePath = join(tempDir, 'emails.jsonl')
|
|
173
|
+
process.env.OM_TEST_MODE = '1'
|
|
174
|
+
process.env.OM_TEST_EMAIL_CAPTURE_PATH = capturePath
|
|
175
|
+
delete process.env.RESEND_API_KEY
|
|
176
|
+
|
|
177
|
+
await sendEmail({
|
|
178
|
+
to: 'user@example.com',
|
|
179
|
+
subject: 'Invite',
|
|
180
|
+
react: React.createElement('div', null, [
|
|
181
|
+
React.createElement('p', { key: 'text' }, 'Accept your invite'),
|
|
182
|
+
React.createElement('a', { key: 'link', href: 'https://example.com/portal/invite?token=raw' }, 'Accept'),
|
|
183
|
+
]),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const rows = (await readFile(capturePath, 'utf8')).trim().split('\n')
|
|
187
|
+
expect(rows).toHaveLength(1)
|
|
188
|
+
expect(JSON.parse(rows[0])).toEqual(expect.objectContaining({
|
|
189
|
+
to: 'user@example.com',
|
|
190
|
+
subject: 'Invite',
|
|
191
|
+
links: ['https://example.com/portal/invite?token=raw'],
|
|
192
|
+
text: 'Accept your invite Accept',
|
|
193
|
+
}))
|
|
194
|
+
expect(ResendMock).not.toHaveBeenCalled()
|
|
195
|
+
expect(sendMock).not.toHaveBeenCalled()
|
|
196
|
+
})
|
|
161
197
|
})
|
package/src/lib/email/send.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Resend } from 'resend'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { appendFile, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
3
6
|
import { parseBooleanWithDefault } from '../boolean'
|
|
4
7
|
import { resolveDefaultEmailFromAddress } from './config'
|
|
5
8
|
|
|
@@ -16,10 +19,86 @@ export type SendEmailOptions = {
|
|
|
16
19
|
}>
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
type CapturedEmail = {
|
|
23
|
+
to: string
|
|
24
|
+
subject: string
|
|
25
|
+
from: string | null
|
|
26
|
+
replyTo: string | null
|
|
27
|
+
links: string[]
|
|
28
|
+
text: string
|
|
29
|
+
capturedAt: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ReactElementProps = {
|
|
33
|
+
href?: unknown
|
|
34
|
+
children?: unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_TEST_EMAIL_CAPTURE_PATH = join(tmpdir(), 'open-mercato-email-capture.jsonl')
|
|
38
|
+
|
|
39
|
+
function resolveTestEmailCapturePath(): string {
|
|
40
|
+
return process.env.OM_TEST_EMAIL_CAPTURE_PATH?.trim() || DEFAULT_TEST_EMAIL_CAPTURE_PATH
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readElementProps(node: React.ReactElement): ReactElementProps {
|
|
44
|
+
return node.props as ReactElementProps
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function collectEmailLinks(node: unknown, links: string[] = []): string[] {
|
|
48
|
+
if (node == null || typeof node === 'boolean') return links
|
|
49
|
+
if (Array.isArray(node)) {
|
|
50
|
+
for (const child of node) collectEmailLinks(child, links)
|
|
51
|
+
return links
|
|
52
|
+
}
|
|
53
|
+
if (React.isValidElement(node)) {
|
|
54
|
+
const props = readElementProps(node)
|
|
55
|
+
if (typeof props.href === 'string' && props.href.length > 0) links.push(props.href)
|
|
56
|
+
collectEmailLinks(props.children, links)
|
|
57
|
+
}
|
|
58
|
+
return links
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectEmailText(node: unknown, parts: string[] = []): string[] {
|
|
62
|
+
if (node == null || typeof node === 'boolean') return parts
|
|
63
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
64
|
+
parts.push(String(node))
|
|
65
|
+
return parts
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(node)) {
|
|
68
|
+
for (const child of node) collectEmailText(child, parts)
|
|
69
|
+
return parts
|
|
70
|
+
}
|
|
71
|
+
if (React.isValidElement(node)) {
|
|
72
|
+
collectEmailText(readElementProps(node).children, parts)
|
|
73
|
+
}
|
|
74
|
+
return parts
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function captureEmailForTests(options: SendEmailOptions): Promise<void> {
|
|
78
|
+
if (!parseBooleanWithDefault(process.env.OM_TEST_MODE, false)) return
|
|
79
|
+
|
|
80
|
+
const capturePath = resolveTestEmailCapturePath()
|
|
81
|
+
const record: CapturedEmail = {
|
|
82
|
+
to: options.to,
|
|
83
|
+
subject: options.subject,
|
|
84
|
+
from: options.from ?? resolveDefaultEmailFromAddress() ?? null,
|
|
85
|
+
replyTo: options.replyTo ?? null,
|
|
86
|
+
links: collectEmailLinks(options.react),
|
|
87
|
+
text: collectEmailText(options.react).join(' ').replace(/\s+/g, ' ').trim(),
|
|
88
|
+
capturedAt: new Date().toISOString(),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await mkdir(dirname(capturePath), { recursive: true })
|
|
92
|
+
await appendFile(capturePath, `${JSON.stringify(record)}\n`, 'utf8')
|
|
93
|
+
}
|
|
94
|
+
|
|
19
95
|
export async function sendEmail({ to, subject, react, from, replyTo, attachments }: SendEmailOptions) {
|
|
20
96
|
const emailDisabled =
|
|
21
97
|
parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) ||
|
|
22
98
|
parseBooleanWithDefault(process.env.OM_TEST_MODE, false)
|
|
99
|
+
|
|
100
|
+
await captureEmailForTests({ to, subject, react, from, replyTo, attachments })
|
|
101
|
+
|
|
23
102
|
if (emailDisabled) return
|
|
24
103
|
|
|
25
104
|
const apiKey = process.env.RESEND_API_KEY
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
} from '../subscriber'
|
|
5
5
|
import type { TenantDataEncryptionService } from '../tenantDataEncryptionService'
|
|
6
6
|
|
|
7
|
-
// Give every entity a resolvable id so the real decrypt() path proceeds to decryptEntityPayload
|
|
7
|
+
// Give every entity a resolvable id so the real decrypt() path proceeds to decryptEntityPayload,
|
|
8
|
+
// keying off the class name so a test's fixture and its assertion describe the same entity.
|
|
8
9
|
jest.mock('../entityIds', () => ({
|
|
9
|
-
resolveEntityIdFromMetadata: jest.fn(() =>
|
|
10
|
+
resolveEntityIdFromMetadata: jest.fn((meta?: { className?: string }) =>
|
|
11
|
+
meta?.className === 'OnboardingRequest' ? 'onboarding:onboarding_request' : 'customers:customer_address'),
|
|
10
12
|
}))
|
|
11
13
|
|
|
12
14
|
describe('decryptEntitiesWithFallbackScope subscriber memoization (issue #2235)', () => {
|
|
@@ -150,3 +152,53 @@ describe('decryptEntitiesWithFallbackScope per-row scope (multi-org safety)', ()
|
|
|
150
152
|
expect(calls).toHaveLength(3)
|
|
151
153
|
})
|
|
152
154
|
})
|
|
155
|
+
|
|
156
|
+
describe('tenant-less entity encryption delegation', () => {
|
|
157
|
+
const originalToggle = process.env.TENANT_DATA_ENCRYPTION
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
process.env.TENANT_DATA_ENCRYPTION = 'yes'
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
afterEach(() => {
|
|
164
|
+
if (originalToggle === undefined) delete process.env.TENANT_DATA_ENCRYPTION
|
|
165
|
+
else process.env.TENANT_DATA_ENCRYPTION = originalToggle
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('lets the encryption service resolve an explicit system-scoped map', async () => {
|
|
169
|
+
const encryptEntityPayload = jest.fn(async (
|
|
170
|
+
_entityId: string,
|
|
171
|
+
target: Record<string, unknown>,
|
|
172
|
+
) => ({ ...target, secret: 'encrypted' }))
|
|
173
|
+
const service = {
|
|
174
|
+
isEnabled: () => true,
|
|
175
|
+
encryptEntityPayload,
|
|
176
|
+
} as unknown as TenantDataEncryptionService
|
|
177
|
+
const subscriber = new TenantEncryptionSubscriber(service)
|
|
178
|
+
const entity = { secret: 'plaintext' }
|
|
179
|
+
const meta = {
|
|
180
|
+
className: 'OnboardingRequest',
|
|
181
|
+
tableName: 'onboarding_requests',
|
|
182
|
+
properties: {
|
|
183
|
+
secret: { name: 'secret', fieldName: 'secret' },
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
const changeSet = { payload: { secret: 'plaintext' } }
|
|
187
|
+
|
|
188
|
+
await subscriber.beforeCreate({
|
|
189
|
+
entity,
|
|
190
|
+
meta,
|
|
191
|
+
em: {},
|
|
192
|
+
changeSet,
|
|
193
|
+
} as never)
|
|
194
|
+
|
|
195
|
+
expect(encryptEntityPayload).toHaveBeenCalledWith(
|
|
196
|
+
'onboarding:onboarding_request',
|
|
197
|
+
expect.any(Object),
|
|
198
|
+
null,
|
|
199
|
+
null,
|
|
200
|
+
)
|
|
201
|
+
expect(entity.secret).toBe('encrypted')
|
|
202
|
+
expect(changeSet.payload.secret).toBe('encrypted')
|
|
203
|
+
})
|
|
204
|
+
})
|