mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat(spring): model AOP advice and proxy behavior * fix(spring): address AOP review findings --------- Co-authored-by: Shining <xuenning@qiyi.com>
228 lines
8.5 KiB
TypeScript
228 lines
8.5 KiB
TypeScript
/**
|
|
* P0 Integration Tests: Local Backend
|
|
*
|
|
* Tests tool implementations via direct LadybugDB queries.
|
|
* The full LocalBackend.callTool() requires a global registry,
|
|
* so here we test the security-critical behaviors directly:
|
|
* - Query execution via the pool
|
|
* - Parameterized queries preventing injection
|
|
* - Read-only enforcement
|
|
*
|
|
* Covers hardening fixes: #1 (parameterized queries), #3 (path traversal),
|
|
* #4 (relation allowlist), #26 (rename first-occurrence-only)
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
initLbug,
|
|
closeLbug,
|
|
executeQuery,
|
|
executeParameterized,
|
|
} from '../../src/mcp/core/lbug-adapter.js';
|
|
import { VALID_RELATION_TYPES } from '../../src/mcp/local/local-backend.js';
|
|
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
|
import { LOCAL_BACKEND_SEED_DATA } from '../fixtures/local-backend-seed.js';
|
|
|
|
// ─── Block 1: Pool adapter tests ─────────────────────────────────────
|
|
|
|
withTestLbugDB(
|
|
'local-backend',
|
|
(handle) => {
|
|
it('allows valid read queries through the pool', async () => {
|
|
const rows = await executeQuery(
|
|
handle.repoId,
|
|
'MATCH (n:Function) RETURN n.name AS name ORDER BY n.name',
|
|
);
|
|
expect(rows.length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
// ─── Parameterized queries ───────────────────────────────────────────
|
|
|
|
describe('parameterized queries', () => {
|
|
it('finds exact match with parameter', async () => {
|
|
const rows = await executeParameterized(
|
|
handle.repoId,
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name, n.filePath AS filePath',
|
|
{ name: 'login' },
|
|
);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].name).toBe('login');
|
|
expect(rows[0].filePath).toBe('src/auth.ts');
|
|
});
|
|
|
|
it('injection is harmless', async () => {
|
|
const rows = await executeParameterized(
|
|
handle.repoId,
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
|
{ name: "login' OR '1'='1" },
|
|
);
|
|
expect(rows).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ─── Relation type filtering ─────────────────────────────────────────
|
|
|
|
describe('relation type filtering', () => {
|
|
it('only allows valid relation types in queries', () => {
|
|
const validTypes = [
|
|
'CALLS',
|
|
'IMPORTS',
|
|
'EXTENDS',
|
|
'IMPLEMENTS',
|
|
'HAS_METHOD',
|
|
'METHOD_OVERRIDES',
|
|
'ACCESSES',
|
|
'ADVISED_BY',
|
|
];
|
|
const invalidTypes = ['CONTAINS', 'STEP_IN_PROCESS', 'MEMBER_OF', 'DROP_TABLE'];
|
|
|
|
for (const t of validTypes) {
|
|
expect(VALID_RELATION_TYPES.has(t)).toBe(true);
|
|
}
|
|
for (const t of invalidTypes) {
|
|
expect(VALID_RELATION_TYPES.has(t)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('can query relationships with valid types', async () => {
|
|
const rows = await executeQuery(
|
|
handle.repoId,
|
|
`MATCH (a:Function)-[r:CodeRelation {type: 'CALLS'}]->(b:Function) RETURN a.name AS caller, b.name AS callee ORDER BY b.name`,
|
|
);
|
|
expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|
|
|
|
// ─── Process queries ─────────────────────────────────────────────────
|
|
|
|
describe('process queries', () => {
|
|
it('can find processes', async () => {
|
|
const rows = await executeQuery(
|
|
handle.repoId,
|
|
'MATCH (p:Process) RETURN p.heuristicLabel AS label, p.stepCount AS steps',
|
|
);
|
|
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
expect(rows[0].label).toBe('User Login');
|
|
});
|
|
|
|
it('can trace process steps', async () => {
|
|
const rows = await executeQuery(
|
|
handle.repoId,
|
|
`MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
WHERE p.id = 'proc:login-flow'
|
|
RETURN s.name AS symbol, r.step AS step
|
|
ORDER BY r.step`,
|
|
);
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0].symbol).toBe('login');
|
|
expect(rows[0].step).toBe(1);
|
|
expect(rows[1].symbol).toBe('validate');
|
|
expect(rows[1].step).toBe(2);
|
|
});
|
|
});
|
|
|
|
// ─── Community queries ───────────────────────────────────────────────
|
|
|
|
describe('community queries', () => {
|
|
it('can find communities', async () => {
|
|
const rows = await executeQuery(
|
|
handle.repoId,
|
|
'MATCH (c:Community) RETURN c.heuristicLabel AS label',
|
|
);
|
|
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
expect(rows[0].label).toBe('Authentication');
|
|
});
|
|
|
|
it('can find community members', async () => {
|
|
const rows = await executeQuery(
|
|
handle.repoId,
|
|
`MATCH (f)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
WHERE c.heuristicLabel = 'Authentication'
|
|
RETURN f.name AS name`,
|
|
);
|
|
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
expect(rows[0].name).toBe('login');
|
|
});
|
|
});
|
|
|
|
// ─── Read-only enforcement ───────────────────────────────────────────
|
|
|
|
describe('read-only database', () => {
|
|
it('keeps seeded rows unchanged for a no-match write probe', async () => {
|
|
const readOnlyRepo = 'local-backend-read-only';
|
|
await initLbug(readOnlyRepo, handle.dbPath);
|
|
try {
|
|
const rows = await executeParameterized(
|
|
readOnlyRepo,
|
|
`MATCH (n:Function) WHERE n.name = $target SET n.name = $name RETURN n.name AS name`,
|
|
{ target: '__missing__', name: 'changed' },
|
|
);
|
|
expect(rows).toEqual([]);
|
|
} catch (err) {
|
|
expect(String(err)).toMatch(/Write operations are not allowed|read-only database/i);
|
|
}
|
|
const rows = await executeParameterized(
|
|
readOnlyRepo,
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
|
{ name: 'login' },
|
|
);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].name).toBe('login');
|
|
await closeLbug(readOnlyRepo);
|
|
});
|
|
});
|
|
|
|
// ─── Content queries (include_content equivalent) ────────────────────
|
|
|
|
describe('content queries', () => {
|
|
it('can retrieve symbol content', async () => {
|
|
const rows = await executeQuery(
|
|
handle.repoId,
|
|
`MATCH (n:Function) WHERE n.name = 'login' RETURN n.content AS content`,
|
|
);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].content).toContain('function login');
|
|
});
|
|
});
|
|
|
|
// ─── Query error handling via pool ──────────────────────────────────
|
|
|
|
describe('query error handling via pool', () => {
|
|
it('returns empty rows for unknown node label', async () => {
|
|
// LadybugDB throws a Binder exception for unknown node labels
|
|
await expect(
|
|
executeQuery(handle.repoId, 'MATCH (n:NonExistentTable) RETURN n.name AS name'),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('rejects syntactically invalid Cypher', async () => {
|
|
await expect(executeQuery(handle.repoId, 'NOT VALID CYPHER AT ALL')).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── Parameterized query edge cases ─────────────────────────────────
|
|
|
|
describe('parameterized query edge cases', () => {
|
|
it('succeeds with empty params when query has no parameters', async () => {
|
|
const rows = await executeParameterized(
|
|
handle.repoId,
|
|
'MATCH (n:Function) RETURN n.name AS name LIMIT 1',
|
|
{},
|
|
);
|
|
expect(rows.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('returns empty rows when param value is null', async () => {
|
|
const rows = await executeParameterized(
|
|
handle.repoId,
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
|
{ name: null as any },
|
|
);
|
|
expect(rows).toHaveLength(0);
|
|
});
|
|
});
|
|
},
|
|
{
|
|
seed: LOCAL_BACKEND_SEED_DATA,
|
|
poolAdapter: true,
|
|
},
|
|
);
|