323 lines
10 KiB
TypeScript
323 lines
10 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
|
|
import { createMigrationRunner } from '../ts_migrations/index.js';
|
|
|
|
function setPath(target: Record<string, any>, path: string, value: unknown): void {
|
|
const parts = path.split('.');
|
|
let cursor = target;
|
|
for (const part of parts.slice(0, -1)) {
|
|
cursor[part] = cursor[part] || {};
|
|
cursor = cursor[part];
|
|
}
|
|
cursor[parts[parts.length - 1]] = value;
|
|
}
|
|
|
|
function getPath(target: Record<string, any>, path: string): unknown {
|
|
let cursor: any = target;
|
|
for (const part of path.split('.')) {
|
|
if (cursor === null || cursor === undefined) return undefined;
|
|
cursor = cursor[part];
|
|
}
|
|
return cursor;
|
|
}
|
|
|
|
function applySet(document: Record<string, any>, set: Record<string, unknown>): void {
|
|
for (const [key, value] of Object.entries(set)) {
|
|
setPath(document, key, value);
|
|
}
|
|
}
|
|
|
|
function unsetPath(target: Record<string, any>, path: string): void {
|
|
const parts = path.split('.');
|
|
let cursor: any = target;
|
|
for (const part of parts.slice(0, -1)) {
|
|
if (cursor?.[part] === undefined) return;
|
|
cursor = cursor[part];
|
|
}
|
|
if (cursor && typeof cursor === 'object') {
|
|
delete cursor[parts[parts.length - 1]];
|
|
}
|
|
}
|
|
|
|
function applyUnset(document: Record<string, any>, unset: Record<string, unknown>): void {
|
|
for (const key of Object.keys(unset)) {
|
|
unsetPath(document, key);
|
|
}
|
|
}
|
|
|
|
function matchesQuery(document: Record<string, any>, query: Record<string, any>): boolean {
|
|
for (const [key, expected] of Object.entries(query)) {
|
|
const actual = getPath(document, key);
|
|
if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
|
|
if ('$exists' in expected) {
|
|
const exists = actual !== undefined;
|
|
if (exists !== Boolean(expected.$exists)) return false;
|
|
continue;
|
|
}
|
|
if ('$type' in expected) {
|
|
if (expected.$type === 'string' && typeof actual !== 'string') return false;
|
|
continue;
|
|
}
|
|
if ('$in' in expected) {
|
|
if (!Array.isArray(expected.$in) || !expected.$in.includes(actual)) return false;
|
|
continue;
|
|
}
|
|
}
|
|
if (actual !== expected) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function createFakeCollection(documents: Array<Record<string, any>> = []) {
|
|
return {
|
|
findOne: async (query: Record<string, any> = {}) => {
|
|
const document = documents.find((candidate) => matchesQuery(candidate, query));
|
|
return document ? structuredClone(document) : null;
|
|
},
|
|
find: (query: Record<string, any> = {}) => ({
|
|
async *[Symbol.asyncIterator]() {
|
|
for (const document of documents) {
|
|
if (matchesQuery(document, query)) {
|
|
yield structuredClone(document);
|
|
}
|
|
}
|
|
},
|
|
}),
|
|
insertOne: async (document: Record<string, any>) => {
|
|
documents.push(structuredClone(document));
|
|
return { insertedId: document._id || document.id };
|
|
},
|
|
updateMany: async (query: Record<string, any>, update: any) => {
|
|
let modifiedCount = 0;
|
|
for (const document of documents) {
|
|
if (!matchesQuery(document, query)) continue;
|
|
applySet(document, update.$set || {});
|
|
applyUnset(document, update.$unset || {});
|
|
modifiedCount++;
|
|
}
|
|
return { modifiedCount };
|
|
},
|
|
updateOne: async (query: Record<string, any>, update: any) => {
|
|
const document = documents.find((candidate) => matchesQuery(candidate, query));
|
|
if (!document) return { matchedCount: 0, modifiedCount: 0, upsertedCount: 0 };
|
|
applySet(document, update.$set || {});
|
|
applyUnset(document, update.$unset || {});
|
|
return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };
|
|
},
|
|
};
|
|
}
|
|
|
|
function createFakeDb(
|
|
currentVersion: string,
|
|
collections: Record<string, Array<Record<string, any>>> = {},
|
|
) {
|
|
const ledgerDocument = {
|
|
nameId: 'smartmigration:smartmigration',
|
|
data: {
|
|
currentVersion,
|
|
steps: {},
|
|
lock: { holder: null, acquiredAt: null, expiresAt: null },
|
|
checkpoints: {},
|
|
},
|
|
};
|
|
|
|
const fakeCollections = new Map(
|
|
Object.entries(collections).map(([name, documents]) => [name, createFakeCollection(documents)]),
|
|
);
|
|
const emptyCollection = createFakeCollection();
|
|
|
|
const ledgerCollection = {
|
|
createIndex: async () => undefined,
|
|
findOne: async () => structuredClone(ledgerDocument),
|
|
findOneAndUpdate: async (_query: unknown, update: any) => {
|
|
applySet(ledgerDocument, update.$set || {});
|
|
return structuredClone(ledgerDocument);
|
|
},
|
|
updateOne: async (_query: unknown, update: any) => {
|
|
applySet(ledgerDocument, update.$set || {});
|
|
return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };
|
|
},
|
|
};
|
|
|
|
return {
|
|
mongoDb: {
|
|
collection: (name: string) =>
|
|
name === 'SmartdataEasyStore'
|
|
? ledgerCollection
|
|
: fakeCollections.get(name) || emptyCollection,
|
|
},
|
|
};
|
|
}
|
|
|
|
tap.test('migration runner applies schema steps through the current target', async () => {
|
|
const sourceProfiles: Array<Record<string, any>> = [];
|
|
const runner = await createMigrationRunner(
|
|
createFakeDb('13.16.0', { SourceProfileDoc: sourceProfiles }),
|
|
'13.42.0',
|
|
);
|
|
const result = await runner.run();
|
|
|
|
expect(result.currentVersionBefore).toEqual('13.16.0');
|
|
expect(result.currentVersionAfter).toEqual('13.42.0');
|
|
expect(result.stepsApplied).toHaveLength(4);
|
|
expect(sourceProfiles.map((profile) => profile.name)).toContain('TRUSTED NETWORKS');
|
|
expect(sourceProfiles.map((profile) => profile.name)).toContain('AI CRAWLERS');
|
|
expect(sourceProfiles.map((profile) => profile.name)).toContain('PUBLIC');
|
|
});
|
|
|
|
tap.test('migration runner rematerializes source-profile-backed route security', async () => {
|
|
const profiles: Array<Record<string, any>> = [
|
|
{
|
|
_id: 'profile-doc-1',
|
|
id: 'standard-profile',
|
|
name: 'Standard',
|
|
security: {
|
|
ipAllowList: ['192.168.*', '127.0.0.1'],
|
|
maxConnections: 1000,
|
|
},
|
|
},
|
|
];
|
|
const routes: Array<Record<string, any>> = [
|
|
{
|
|
_id: 'route-doc-1',
|
|
id: 'route-1',
|
|
route: {
|
|
name: 'Public service domains',
|
|
match: { ports: 443, domains: ['code.foss.global'] },
|
|
action: { type: 'forward', targets: [{ host: '192.168.5.247', port: 443 }] },
|
|
security: {
|
|
ipAllowList: ['192.168.*', '*'],
|
|
maxConnections: 1000,
|
|
},
|
|
},
|
|
metadata: {
|
|
sourceProfileRef: 'standard-profile',
|
|
sourceProfileName: 'Standard',
|
|
},
|
|
updatedAt: 1,
|
|
},
|
|
];
|
|
|
|
const runner = await createMigrationRunner(
|
|
createFakeDb('13.40.1', {
|
|
SourceProfileDoc: profiles,
|
|
RouteDoc: routes,
|
|
}),
|
|
'13.40.2',
|
|
);
|
|
const result = await runner.run();
|
|
|
|
expect(result.stepsApplied).toHaveLength(1);
|
|
expect(routes[0].route.security.ipAllowList.includes('*')).toBeFalse();
|
|
expect(routes[0].route.security.ipAllowList).toContain('192.168.*');
|
|
expect(routes[0].route.security.maxConnections).toEqual(1000);
|
|
expect(routes[0].metadata.lastResolvedAt).toBeTruthy();
|
|
});
|
|
|
|
tap.test('migration runner seeds only missing default source profiles', async () => {
|
|
const sourceProfiles: Array<Record<string, any>> = [
|
|
{
|
|
id: 'public-profile',
|
|
name: 'PUBLIC',
|
|
description: 'Existing public profile',
|
|
security: { ipAllowList: ['*'] },
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
createdBy: 'test',
|
|
},
|
|
];
|
|
|
|
const runner = await createMigrationRunner(
|
|
createFakeDb('13.40.2', { SourceProfileDoc: sourceProfiles }),
|
|
'13.42.0',
|
|
);
|
|
const result = await runner.run();
|
|
|
|
const publicProfiles = sourceProfiles.filter((profile) => profile.name === 'PUBLIC');
|
|
expect(result.stepsApplied).toHaveLength(1);
|
|
expect(sourceProfiles).toHaveLength(3);
|
|
expect(publicProfiles).toHaveLength(1);
|
|
expect(publicProfiles[0].security.rateLimit).toBeUndefined();
|
|
expect(sourceProfiles.map((profile) => profile.name)).toContain('TRUSTED NETWORKS');
|
|
expect(sourceProfiles.map((profile) => profile.name)).toContain('AI CRAWLERS');
|
|
});
|
|
|
|
tap.test('migration runner converts legacy route access metadata to source bindings', async () => {
|
|
const profiles: Array<Record<string, any>> = [
|
|
{
|
|
_id: 'profile-doc-1',
|
|
id: 'standard-profile',
|
|
name: 'Standard',
|
|
security: { ipAllowList: ['10.0.0.0/8'] },
|
|
},
|
|
{
|
|
_id: 'profile-doc-2',
|
|
id: 'public-profile',
|
|
name: 'PUBLIC',
|
|
security: { ipAllowList: ['*'] },
|
|
},
|
|
];
|
|
const routes: Array<Record<string, any>> = [
|
|
{
|
|
_id: 'route-doc-1',
|
|
id: 'route-1',
|
|
route: {
|
|
name: 'standard service',
|
|
match: { ports: 443, domains: ['onebox.example.com'] },
|
|
action: { type: 'forward', targets: [{ host: '10.0.0.2', port: 443 }] },
|
|
security: { ipAllowList: ['10.0.0.0/8'], maxConnections: 1000 },
|
|
},
|
|
metadata: {
|
|
sourceProfileRef: 'standard-profile',
|
|
sourceProfileName: 'Old Standard Name',
|
|
},
|
|
updatedAt: 1,
|
|
},
|
|
{
|
|
_id: 'route-doc-2',
|
|
id: 'route-2',
|
|
route: {
|
|
name: 'gitea',
|
|
match: { ports: 443, domains: ['code.example.com'] },
|
|
action: { type: 'forward', targets: [{ host: '10.0.0.3', port: 3000 }] },
|
|
security: { basicAuth: { username: 'user', password: 'pass' } },
|
|
},
|
|
metadata: {
|
|
sourcePolicy: {
|
|
bindings: [
|
|
{ sourceProfileRef: 'standard-profile' },
|
|
{ sourceProfileRef: 'public-profile' },
|
|
],
|
|
},
|
|
},
|
|
updatedAt: 1,
|
|
},
|
|
];
|
|
|
|
const runner = await createMigrationRunner(
|
|
createFakeDb('13.43.1', {
|
|
SourceProfileDoc: profiles,
|
|
RouteDoc: routes,
|
|
}),
|
|
'13.43.2',
|
|
);
|
|
const result = await runner.run();
|
|
|
|
expect(result.stepsApplied).toHaveLength(1);
|
|
expect(routes[0].metadata.sourceBindings).toEqual([
|
|
{ sourceProfileRef: 'standard-profile', sourceProfileName: 'Old Standard Name' },
|
|
]);
|
|
expect(routes[0].metadata.sourceProfileRef).toBeUndefined();
|
|
expect(routes[0].metadata.sourceProfileName).toBeUndefined();
|
|
expect(routes[0].metadata.sourcePolicy).toBeUndefined();
|
|
expect(routes[0].route.security).toBeUndefined();
|
|
expect(routes[1].metadata.sourceBindings).toEqual([
|
|
{ sourceProfileRef: 'standard-profile', sourceProfileName: 'Standard' },
|
|
{ sourceProfileRef: 'public-profile', sourceProfileName: 'PUBLIC' },
|
|
]);
|
|
expect(routes[1].metadata.sourcePolicy).toBeUndefined();
|
|
expect(routes[1].route.security.basicAuth.username).toEqual('user');
|
|
});
|
|
|
|
export default tap.start();
|