|
| 1 | +import assert from 'node:assert/strict'; |
| 2 | +import { describe, it } from 'node:test'; |
| 3 | +import { isProductionEnvironment } from '../../../src/adapters/mongodb/common/drop-database-safety'; |
| 4 | +import { Logger } from '../../../src/core/logger'; |
| 5 | +import { |
| 6 | + buildCacheInvalidationBarrierKey, |
| 7 | + clearCacheInvalidationBarrier, |
| 8 | + extractCacheInvalidationBarrierNamespaces, |
| 9 | + isCacheInvalidationBarrierActive, |
| 10 | + markCacheInvalidationBarrier, |
| 11 | +} from '../../../src/core/cache-invalidation-barrier'; |
| 12 | +import { createRuntimeAccessors, createRuntimeDbFacade } from '../../../src/entry/runtime-db-facade'; |
| 13 | +import { disposeRuntimeSchemaDslEngine } from '../../../src/entry/runtime-schema-dsl'; |
| 14 | +import { prepareSshTunnelConnectConfig } from '../../../src/entry/runtime-ssh'; |
| 15 | + |
| 16 | +describe('runtime safety helpers', () => { |
| 17 | + it('normalizes production-like environment names', () => { |
| 18 | + assert.equal(isProductionEnvironment(42), false); |
| 19 | + assert.equal(isProductionEnvironment(' PROD '), true); |
| 20 | + assert.equal(isProductionEnvironment('live'), true); |
| 21 | + assert.equal(isProductionEnvironment('staging'), false); |
| 22 | + }); |
| 23 | + |
| 24 | + it('extracts direct namespaces and supported wildcard cache patterns', () => { |
| 25 | + assert.deepEqual(extractCacheInvalidationBarrierNamespaces([ |
| 26 | + '', |
| 27 | + 'db.items', |
| 28 | + 'find:db.items:*', |
| 29 | + 'count:db.items:*', |
| 30 | + 'unsupported:db.other:*', |
| 31 | + 'invalid*pattern', |
| 32 | + ]), ['db.items']); |
| 33 | + assert.equal(buildCacheInvalidationBarrierKey('db.items'), 'cacheDirty:db.items'); |
| 34 | + }); |
| 35 | + |
| 36 | + it('marks barriers only when the cache supports set', async () => { |
| 37 | + assert.deepEqual(await markCacheInvalidationBarrier(null, ['db.items']), []); |
| 38 | + assert.deepEqual(await markCacheInvalidationBarrier({}, ['db.items']), []); |
| 39 | + |
| 40 | + const writes: Array<[string, unknown, number]> = []; |
| 41 | + const namespaces = await markCacheInvalidationBarrier({ |
| 42 | + set: async (key, value, ttl) => { writes.push([key, value, ttl]); }, |
| 43 | + }, ['find:db.items:*', 'db.other'], 1234); |
| 44 | + |
| 45 | + assert.deepEqual(namespaces, ['db.items', 'db.other']); |
| 46 | + assert.equal(writes.length, 2); |
| 47 | + assert.deepEqual(writes.map(([key, , ttl]) => [key, ttl]), [ |
| 48 | + ['cacheDirty:db.items', 1234], |
| 49 | + ['cacheDirty:db.other', 1234], |
| 50 | + ]); |
| 51 | + }); |
| 52 | + |
| 53 | + it('clears barriers through del or delete and accepts incapable caches', async () => { |
| 54 | + await assert.doesNotReject(() => clearCacheInvalidationBarrier(null, ['db.items'])); |
| 55 | + await assert.doesNotReject(() => clearCacheInvalidationBarrier({}, ['db.items'])); |
| 56 | + |
| 57 | + const deleted: string[] = []; |
| 58 | + await clearCacheInvalidationBarrier({ del: async (key) => { deleted.push(`del:${key}`); } }, ['db.items']); |
| 59 | + await clearCacheInvalidationBarrier({ delete: async (key) => { deleted.push(`delete:${key}`); } }, ['count:db.other:*']); |
| 60 | + assert.deepEqual(deleted, ['del:cacheDirty:db.items', 'delete:cacheDirty:db.other']); |
| 61 | + }); |
| 62 | + |
| 63 | + it('detects only truthy barrier markers', async () => { |
| 64 | + assert.equal(await isCacheInvalidationBarrierActive(null, ['db.items']), false); |
| 65 | + assert.equal(await isCacheInvalidationBarrierActive({}, ['db.items']), false); |
| 66 | + |
| 67 | + const values = new Map<string, unknown>([ |
| 68 | + ['cacheDirty:db.undefined', undefined], |
| 69 | + ['cacheDirty:db.null', null], |
| 70 | + ['cacheDirty:db.false', false], |
| 71 | + ['cacheDirty:db.active', { dirty: true }], |
| 72 | + ]); |
| 73 | + const cache = { get: async (key: string) => values.get(key) }; |
| 74 | + assert.equal(await isCacheInvalidationBarrierActive(cache, ['db.undefined', 'db.null', 'db.false']), false); |
| 75 | + assert.equal(await isCacheInvalidationBarrierActive(cache, ['db.undefined', 'db.active', 'db.false']), true); |
| 76 | + }); |
| 77 | + |
| 78 | + it('contains schema runtime disposal errors with or without a warning sink', () => { |
| 79 | + const warnings: unknown[][] = []; |
| 80 | + const engine = { dispose: () => { throw new Error('dispose failed'); } }; |
| 81 | + assert.doesNotThrow(() => disposeRuntimeSchemaDslEngine(engine as never, { |
| 82 | + warn: (...args: unknown[]) => { warnings.push(args); }, |
| 83 | + }, 'during close')); |
| 84 | + assert.equal(warnings.length, 1); |
| 85 | + assert.doesNotThrow(() => disposeRuntimeSchemaDslEngine(engine as never, {}, 'before reconnect')); |
| 86 | + }); |
| 87 | + |
| 88 | + it('keeps missing SSH configuration unchanged', async () => { |
| 89 | + const logger = Logger.create(null); |
| 90 | + assert.deepEqual(await prepareSshTunnelConnectConfig(undefined, 'db', logger), { |
| 91 | + connectConfig: undefined, |
| 92 | + tunnel: null, |
| 93 | + }); |
| 94 | + const config = { uri: 'mongodb://localhost:27017' }; |
| 95 | + assert.deepEqual(await prepareSshTunnelConnectConfig(config, 'db', logger), { |
| 96 | + connectConfig: config, |
| 97 | + tunnel: null, |
| 98 | + }); |
| 99 | + }); |
| 100 | + |
| 101 | + it('normalizes cache auto-invalidation options for database facades', () => { |
| 102 | + const database = { collection: () => ({}) }; |
| 103 | + const host = (options: Record<string, unknown>) => ({ |
| 104 | + options, |
| 105 | + _client: { db: () => database }, |
| 106 | + _logger: Logger.create(null), |
| 107 | + _runtimeDefaults: {}, |
| 108 | + resolveAdapterCache: () => null, |
| 109 | + }); |
| 110 | + |
| 111 | + assert.ok(createRuntimeDbFacade(host({ cache: { autoInvalidate: true } }) as never, 'db')); |
| 112 | + assert.ok(createRuntimeDbFacade(host({ cache: [], cacheAutoInvalidate: true }) as never, 'db')); |
| 113 | + assert.ok(createRuntimeDbFacade(host({ cache: 'legacy', cacheAutoInvalidate: false }) as never, 'db')); |
| 114 | + }); |
| 115 | + |
| 116 | + it('rejects every invalid collection-name shape from runtime accessors', () => { |
| 117 | + const accessors = createRuntimeAccessors({ |
| 118 | + defaultDb: { collection: () => ({}) } as never, |
| 119 | + runtime: {}, |
| 120 | + db: () => ({}) as never, |
| 121 | + use: () => ({}) as never, |
| 122 | + getIidCache: () => null, |
| 123 | + setIidCache: () => undefined, |
| 124 | + }); |
| 125 | + |
| 126 | + for (const name of ['', 42, ' ']) { |
| 127 | + assert.throws( |
| 128 | + () => accessors.collection(name as string), |
| 129 | + (error: unknown) => error instanceof Error && (error as Error & { code?: string }).code === 'INVALID_COLLECTION_NAME', |
| 130 | + ); |
| 131 | + } |
| 132 | + }); |
| 133 | +}); |
0 commit comments