Files
dockhand-upstream/tests/vulnerabilities-cache.test.ts

119 lines
3.6 KiB
TypeScript

/**
* Unit tests for the vulnerability aggregation cache.
*
* This module is deliberately db-free (it imports only client-safe types) so
* it can be exercised under `bun test` without pulling in better-sqlite3 — the
* whole reason the cache was extracted from vulnerabilities.ts. These tests
* pin down the invalidation semantics that saveVulnerabilityScan relies on.
*/
import { test, expect, beforeEach } from 'bun:test';
import {
aggregateCache,
inflight,
invalidateVulnerabilitiesCache,
CACHE_TTL_MS,
MAX_VIEWS,
type CacheEntry,
type AggregatedVulnerabilities
} from '../src/lib/server/vulnerabilities-cache';
function makeEntry(): CacheEntry {
const data: AggregatedVulnerabilities = {
findings: [],
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0, imagesScanned: 0, totalImages: 0 }
};
return { at: 0, data, views: new Map() };
}
beforeEach(() => {
aggregateCache.clear();
inflight.clear();
});
test('exports sane constants', () => {
expect(CACHE_TTL_MS).toBe(30_000);
expect(MAX_VIEWS).toBe(8);
});
test('invalidate with an env id drops only that environment', () => {
aggregateCache.set(1, makeEntry());
aggregateCache.set(2, makeEntry());
invalidateVulnerabilitiesCache(1);
expect(aggregateCache.has(1)).toBe(false);
expect(aggregateCache.has(2)).toBe(true);
});
test('invalidate with no argument clears every environment', () => {
aggregateCache.set(1, makeEntry());
aggregateCache.set(2, makeEntry());
invalidateVulnerabilitiesCache();
expect(aggregateCache.size).toBe(0);
});
test('invalidate with null clears every environment (unknown-env broad change)', () => {
aggregateCache.set(1, makeEntry());
aggregateCache.set(2, makeEntry());
invalidateVulnerabilitiesCache(null);
expect(aggregateCache.size).toBe(0);
});
test('invalidating a missing env id is a no-op, not an error', () => {
aggregateCache.set(5, makeEntry());
expect(() => invalidateVulnerabilitiesCache(999)).not.toThrow();
expect(aggregateCache.size).toBe(1);
expect(aggregateCache.has(5)).toBe(true);
});
test('null-environment scans key under 0', () => {
// saveVulnerabilityScan passes `data.environmentId ?? undefined`; a null env
// therefore invalidates env 0, which is where the dashboard aggregates it.
aggregateCache.set(0, makeEntry());
invalidateVulnerabilitiesCache(0);
expect(aggregateCache.has(0)).toBe(false);
});
test('invalidating an env also drops its in-flight aggregation', () => {
// A fetch racing a scan-save would otherwise resolve to pre-scan data and be
// installed with a fresh TTL, masking the new scan for a full window.
inflight.set(4, Promise.resolve({
findings: [],
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0, imagesScanned: 0, totalImages: 0 }
}));
inflight.set(5, Promise.resolve({
findings: [],
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0, imagesScanned: 0, totalImages: 0 }
}));
invalidateVulnerabilitiesCache(4);
expect(inflight.has(4)).toBe(false);
expect(inflight.has(5)).toBe(true); // other env untouched
invalidateVulnerabilitiesCache(); // clear-all also clears inflight
expect(inflight.size).toBe(0);
});
test('invalidation drops memoized views along with the entry', () => {
const entry = makeEntry();
entry.views.set('some-query', []);
entry.meta = {
total: 0,
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0, imagesScanned: 0, totalImages: 0 },
options: { images: [], containers: [], stacks: [] }
};
aggregateCache.set(3, entry);
invalidateVulnerabilitiesCache(3);
// Whole entry gone → its views/meta are gone too; a refetch rebuilds them.
expect(aggregateCache.get(3)).toBeUndefined();
});