Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions apps/api/src/scanner/sql-query-scan.job.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Integration: SQL Query scan job -> engine analyzer -> findings persistence.
*/
import * as fs from 'fs';
import * as path from 'path';
import { FindingsRepository } from '../findings/findings.repository';
import { FindingsService } from '../findings/findings.service';
import { MetricsCollector } from '../queue/metrics';
import {
SqlQueryScanJobError,
SqlQueryScanJobDeps,
SqlQueryScanJobHandler,
SqlQueryScanJobPayload,
SqlQueryScanLogger,
} from './sql-query-scan.job';

const FIXTURES = path.resolve(__dirname, '../../../../tests/fixtures/sql-query');

function repoFiles(name: string): Array<{ path: string; content: string }> {
const root = path.join(FIXTURES, name);
const out: Array<{ path: string; content: string }> = [];
const walk = (dir: string) => {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) walk(full);
else out.push({ path: path.relative(root, full).split(path.sep).join('/'), content: fs.readFileSync(full, 'utf8') });
}
};
walk(root);
return out;
}

function captureLogger(): SqlQueryScanLogger & { lines: string[] } {
const lines: string[] = [];
return {
lines,
info: (event, fields) => lines.push(JSON.stringify({ level: 'info', event, ...fields })),
warn: (event, fields) => lines.push(JSON.stringify({ level: 'warn', event, ...fields })),
};
}

function setup(overrides: SqlQueryScanJobDeps = {}) {
const repo = new FindingsRepository();
const findings = new FindingsService(repo);
const logger = captureLogger();
const metrics = new MetricsCollector();
const handler = new SqlQueryScanJobHandler({ findings, logger, metrics, ...overrides });
return { repo, findings, logger, metrics, handler };
}

const job = (over: Partial<SqlQueryScanJobPayload> = {}): SqlQueryScanJobPayload => ({
organizationId: 'org-a',
repositoryId: 'repo-1',
analysisJobId: 'job-1',
files: repoFiles('vulnerable-app'),
...over,
});

async function expectJobError(p: Promise<unknown>, code: string, retryable: boolean) {
const err = await p.then(
() => undefined,
(e) => e,
);
expect(err).toBeInstanceOf(SqlQueryScanJobError);
expect(err).toEqual(expect.objectContaining({ code, retryable }));
}

describe('SqlQueryScanJobHandler', () => {
it('analyzes repository files and persists SQL injection findings', async () => {
const { handler, findings } = setup();
const outcome = await handler.handle(job());

expect(outcome.status).toBe('completed');
expect(outcome.filesReceived).toBeGreaterThanOrEqual(2);
expect(outcome.findingsDetected).toBeGreaterThan(0);
expect(outcome.findingsPersisted).toBe(outcome.findingsDetected);
expect(outcome.findingsAlreadyPersisted).toBe(0);

const page = findings.list({ organizationId: 'org-a', repositoryId: 'repo-1', limit: 100 });
expect(page.items.length).toBe(outcome.findingsPersisted);
expect(page.items.every((f) => f.organizationId === 'org-a' && f.analysisJobId === 'job-1')).toBe(true);
});

it('is idempotent on retry', async () => {
const { handler } = setup();
const first = await handler.handle(job());
expect(first.findingsPersisted).toBeGreaterThan(0);

const second = await handler.handle(job());
expect(second.findingsPersisted).toBe(0);
expect(second.findingsAlreadyPersisted).toBe(first.findingsPersisted);
});

it('rejects payloads with missing tenant IDs', async () => {
const { handler } = setup();
await expectJobError(handler.handle(job({ organizationId: '' })), 'INVALID_JOB', false);
await expectJobError(handler.handle(job({ repositoryId: '' })), 'INVALID_JOB', false);
await expectJobError(handler.handle(job({ analysisJobId: '' })), 'INVALID_JOB', false);
});

it('rejects invalid timeout values', async () => {
const { handler } = setup();
await expectJobError(handler.handle(job({ timeoutMs: -10 })), 'INVALID_JOB', false);
});

it('rejects invalid analyzer configurations', async () => {
const { handler } = setup();
await expectJobError(
handler.handle(
job({
config: {
rules: {
'sql-001': { enabled: true, severity: 'bad_severity' as any },
},
},
}),
),
'INVALID_CONFIG',
false,
);
});
});
Loading
Loading