From 5c868c9e2b14ee36a366bca1f3c79534a5239949 Mon Sep 17 00:00:00 2001 From: Stanislav Mishchyshyn Date: Fri, 12 Jun 2026 14:56:20 +0300 Subject: [PATCH] fix(registry): enhance route validation to include domainAlias for duplicates --- registry/server/config/ConfigService.ts | 7 ++- registry/tests/config.spec.ts | 57 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/registry/server/config/ConfigService.ts b/registry/server/config/ConfigService.ts index 37bd3bed2..b8679b754 100644 --- a/registry/server/config/ConfigService.ts +++ b/registry/server/config/ConfigService.ts @@ -102,7 +102,12 @@ export class ConfigService { Object.values(grouped).forEach((namespaceRoutes) => { const duplicates = namespaceRoutes.filter( (route, idx, arr) => - arr.findIndex((r) => r.route === route.route && r.domainId === route.domainId) !== idx, + arr.findIndex( + (r) => + r.route === route.route && + r.domainId === route.domainId && + r.domainAlias === route.domainAlias, + ) !== idx, ); const unorderedDuplicates = duplicates.filter((route) => typeof route.orderPos !== 'number'); if (unorderedDuplicates.length > 0) { diff --git a/registry/tests/config.spec.ts b/registry/tests/config.spec.ts index 8ebe9fd55..dd4ee6ed2 100644 --- a/registry/tests/config.spec.ts +++ b/registry/tests/config.spec.ts @@ -512,6 +512,28 @@ describe('Tests /api/v1/config', () => { 'Multiple routes with the same "route" and "domainId" and without "orderPos" are present in the passed update:[\n {\n "route": "/unordered/*",\n "slots": {\n "body": {\n "appName": "unordered",\n "props": {\n "c": true\n }\n }\n },\n "namespace": "ns1"\n }\n]To update, ensure that each of these routes has a unique "orderPos" value for the given "domainId".', }); }); + it('should validate same "route" on default and domain-specific (via domainAlias) without orderPos', async () => { + let domainId: number | undefined; + try { + await req.post('/api/v1/template/').send(example.templates).expect(200); + const domainResponse = await req + .post('/api/v1/router_domains/') + .send({ ...example.routerDomains, alias: 'validate-dup-domain' }) + .expect(200); + domainId = domainResponse.body.id; + + await req + .post('/api/v1/config/validate') + .send({ + ...body, + routes: [appRoute('app1'), { ...appRoute('app1'), domainAlias: 'validate-dup-domain' }], + }) + .expect(200, { valid: true }); + } finally { + domainId && (await req.delete(`/api/v1/router_domains/${domainId}`)); + await req.delete('/api/v1/template/' + example.templates.name); + } + }); }); describe('Update', () => { const app = { @@ -1213,5 +1235,40 @@ describe('Tests /api/v1/config', () => { const { body: config } = await req.get('/api/v1/config').expect(200); expect(config.apps['app-alias-fail']).to.be.undefined; }); + it('should upsert same "route" on default and domain-specific (via domainAlias) without orderPos', async () => { + let domainId: number | undefined; + try { + await req.post('/api/v1/template/').send(example.templates).expect(200); + const domainResponse = await req + .post('/api/v1/router_domains/') + .send({ ...example.routerDomains, alias: 'update-dup-domain' }) + .expect(200); + domainId = domainResponse.body.id; + + await req + .put('/api/v1/config') + .send({ + apps: [{ ...app, name: 'app-dup' }], + routes: [appRoute('app-dup'), { ...appRoute('app-dup'), domainAlias: 'update-dup-domain' }], + }) + .expect(204); + + const { body: config } = await req + .get('/api/v1/config') + .query({ domainName: example.routerDomains.domainName }) + .expect(200); + + const dupRoutes = config.routes.filter((r: any) => r.route === '/app-dup/*'); + expect(dupRoutes).to.have.lengthOf(2); + expect(dupRoutes.some((r: any) => r.domain === example.routerDomains.domainName)).to.be.true; + expect(dupRoutes.some((r: any) => r.domain === undefined)).to.be.true; + } finally { + const { body: routesWithId } = await req.get('/api/v1/route'); + await Promise.all(routesWithId.map((x: any) => req.delete(`/api/v1/route/${x.id}`))); + await req.delete('/api/v1/app/app-dup'); + domainId && (await req.delete(`/api/v1/router_domains/${domainId}`)); + await req.delete('/api/v1/template/' + example.templates.name); + } + }); }); });