mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-06 08:16:02 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Classify release event (push) Waiting to run
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
259 lines
9.2 KiB
TypeScript
259 lines
9.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { HttpRouteExtractor } from '../../../src/core/group/extractors/http-route-extractor.js';
|
|
import type { RepoHandle } from '../../../src/core/group/types.js';
|
|
import { DATA_ROUTE_TABLE_SOURCE } from '../../../src/core/ingestion/route-extractors/data-route-table.js';
|
|
|
|
describe('data route table group contracts', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-route-table-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
const repo = (): RepoHandle => ({
|
|
id: 'route-table-test',
|
|
path: 'test/route-table',
|
|
repoPath: tmpDir,
|
|
storagePath: path.join(tmpDir, '.gitnexus'),
|
|
});
|
|
|
|
it('resolves imported handlers and leaves source-only members unattributed', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'routes.js'),
|
|
`import { listUsers as handleUsers } from './handlers.js';
|
|
const routes = [
|
|
{ path: '/users', method: 'GET', handler: handleUsers },
|
|
{ path: '/auth/me', method: 'GET', handler: auth.getCurrentUser },
|
|
];
|
|
for (const route of routes) {
|
|
if (route.path === request.path && route.method === request.method) route.handler();
|
|
}
|
|
`,
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'handlers.js'),
|
|
`export function listUsers() { return []; }
|
|
export const auth = { getCurrentUser() { return {}; } };
|
|
`,
|
|
);
|
|
|
|
const queriedNames: string[] = [];
|
|
const db = async (_query: string, params?: Record<string, unknown>) => {
|
|
const name = String(params?.name ?? '');
|
|
queriedNames.push(name);
|
|
if (name !== 'listUsers' && name !== 'getCurrentUser') return [];
|
|
return [
|
|
{
|
|
uid: `symbol:${name}`,
|
|
name,
|
|
filePath: 'handlers.js',
|
|
startLine: 1,
|
|
endLine: 2,
|
|
labels: ['Function'],
|
|
},
|
|
];
|
|
};
|
|
|
|
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
|
|
const providers = contracts.filter(
|
|
(contract) =>
|
|
contract.role === 'provider' && contract.meta.framework === DATA_ROUTE_TABLE_SOURCE,
|
|
);
|
|
|
|
expect(
|
|
providers
|
|
.map((provider) => ({
|
|
contractId: provider.contractId,
|
|
symbolUid: provider.symbolUid,
|
|
symbolName: provider.symbolName,
|
|
extractionStrategy: provider.meta.extractionStrategy,
|
|
}))
|
|
.sort((left, right) => left.contractId.localeCompare(right.contractId)),
|
|
).toEqual([
|
|
{
|
|
contractId: 'http::GET::/auth/me',
|
|
symbolUid: '',
|
|
symbolName: 'handler',
|
|
extractionStrategy: 'source_scan',
|
|
},
|
|
{
|
|
contractId: 'http::GET::/users',
|
|
symbolUid: 'symbol:listUsers',
|
|
symbolName: 'listUsers',
|
|
extractionStrategy: 'source_scan_resolved',
|
|
},
|
|
]);
|
|
expect(queriedNames).not.toContain('getCurrentUser');
|
|
});
|
|
|
|
it('keeps ambiguous handlers unattributed instead of guessing', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'routes.js'),
|
|
`const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
|
|
for (const route of routes) {
|
|
if (route.path === request.path && route.method === request.method) route.handler();
|
|
}`,
|
|
);
|
|
const db = async (_query: string, params?: Record<string, unknown>) =>
|
|
params?.name === 'listUsers'
|
|
? [
|
|
{ uid: 'a', name: 'listUsers', filePath: 'a.js' },
|
|
{ uid: 'b', name: 'listUsers', filePath: 'b.js' },
|
|
]
|
|
: [];
|
|
|
|
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
|
|
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/users');
|
|
|
|
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'listUsers' });
|
|
});
|
|
|
|
it('keeps duplicate same-file handlers unattributed', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'routes.js'),
|
|
`const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
|
|
for (const route of routes) {
|
|
if (route.path === request.path && route.method === request.method) route.handler();
|
|
}`,
|
|
);
|
|
const db = async (query: string) =>
|
|
query.includes('UNION ALL')
|
|
? [
|
|
{ uid: 'a', name: 'listUsers', filePath: 'routes.js', labels: ['Function'] },
|
|
{ uid: 'b', name: 'listUsers', filePath: 'routes.js', labels: ['Function'] },
|
|
]
|
|
: [];
|
|
|
|
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
|
|
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/users');
|
|
|
|
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'listUsers' });
|
|
});
|
|
|
|
it('does not bind an unimported handler to a unique same-name symbol in another file', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'routes.js'),
|
|
`const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
|
|
for (const route of routes) {
|
|
if (route.path === request.path && route.method === request.method) route.handler();
|
|
}`,
|
|
);
|
|
const db = async (query: string, params?: Record<string, unknown>) => {
|
|
if (query.includes('UNION ALL')) return [];
|
|
return params?.name === 'listUsers'
|
|
? [{ uid: 'decoy', name: 'listUsers', filePath: 'unrelated.js' }]
|
|
: [];
|
|
};
|
|
|
|
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
|
|
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/users');
|
|
|
|
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'listUsers' });
|
|
});
|
|
|
|
it('does not fall back from an unresolved import to a same-name local symbol', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'routes.js'),
|
|
`import { listUsers } from 'external-router-package';
|
|
const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
|
|
for (const route of routes) {
|
|
if (route.path === request.path && route.method === request.method) route.handler();
|
|
}`,
|
|
);
|
|
const db = async (_query: string, params?: Record<string, unknown>) =>
|
|
params?.name === 'listUsers'
|
|
? [{ uid: 'local-decoy', name: 'listUsers', filePath: 'local.js' }]
|
|
: [];
|
|
|
|
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
|
|
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/users');
|
|
|
|
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'listUsers' });
|
|
});
|
|
|
|
it('does not attribute an unresolved member handler to its registrar function', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'routes.js'),
|
|
`function setupRoutes(path, method) {
|
|
const routes = [{ path: '/me', method: 'GET', handler: auth.getCurrentUser }];
|
|
for (const route of routes) {
|
|
if (route.path === request.path && route.method === request.method) route.handler();
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
const db = async (query: string) =>
|
|
query.includes('UNION ALL')
|
|
? [
|
|
{
|
|
uid: 'function:setupRoutes',
|
|
name: 'setupRoutes',
|
|
filePath: 'routes.js',
|
|
startLine: 0,
|
|
endLine: 5,
|
|
labels: ['Function'],
|
|
},
|
|
]
|
|
: [];
|
|
|
|
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
|
|
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/me');
|
|
|
|
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'handler' });
|
|
});
|
|
|
|
it('suppresses duplicate identities when any data-table handler is unresolved', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'routes.js'),
|
|
`function second() {}
|
|
const routes = [
|
|
{ path: '/users', method: 'GET', handler: missing },
|
|
{ path: '/users', method: 'GET', handler: second },
|
|
];
|
|
for (const route of routes) {
|
|
if (route.path === request.path && route.method === request.method) route.handler();
|
|
}`,
|
|
);
|
|
const db = async (query: string) =>
|
|
query.includes('UNION ALL')
|
|
? [{ uid: 'second', name: 'second', filePath: 'routes.js', labels: ['Function'] }]
|
|
: [];
|
|
|
|
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
|
|
|
|
expect(contracts.some((contract) => contract.contractId === 'http::GET::/users')).toBe(false);
|
|
});
|
|
|
|
it('suppresses duplicate identities that resolve to different handlers', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'routes.js'),
|
|
`function first() {}
|
|
function second() {}
|
|
const routes = [
|
|
{ path: '/users', method: 'GET', handler: first },
|
|
{ path: '/users', method: 'GET', handler: second },
|
|
];
|
|
for (const route of routes) {
|
|
if (route.path === request.path && route.method === request.method) route.handler();
|
|
}`,
|
|
);
|
|
const db = async (query: string) =>
|
|
query.includes('UNION ALL')
|
|
? [
|
|
{ uid: 'first', name: 'first', filePath: 'routes.js', labels: ['Function'] },
|
|
{ uid: 'second', name: 'second', filePath: 'routes.js', labels: ['Function'] },
|
|
]
|
|
: [];
|
|
|
|
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
|
|
|
|
expect(contracts.some((contract) => contract.contractId === 'http::GET::/users')).toBe(false);
|
|
});
|
|
});
|