Skip to content

Commit 96bb2ab

Browse files
committed
test: add integration coverage for OSV range reconcile/supersede (CM-1258)
Signed-off-by: Uroš Marolt <uros@marolt.me>
1 parent d139e46 commit 96bb2ab

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
2+
3+
import {
4+
reconcileOsvRanges,
5+
supersedeDepsDevRanges,
6+
} from '@crowd/data-access-layer/src/packages/osv'
7+
import { QueryExecutor, pgpQx } from '@crowd/data-access-layer/src/queryExecutor'
8+
import { getDbConnection } from '@crowd/database'
9+
10+
// Integration test: hits the running packages-db. Skipped automatically when
11+
// any of the DB env vars are missing, matching deriveCriticalFlag.integration.test.ts.
12+
const HAVE_DB =
13+
!!process.env.CROWD_PACKAGES_DB_WRITE_HOST &&
14+
!!process.env.CROWD_PACKAGES_DB_PORT &&
15+
!!process.env.CROWD_PACKAGES_DB_USERNAME &&
16+
!!process.env.CROWD_PACKAGES_DB_DATABASE &&
17+
!!process.env.CROWD_PACKAGES_DB_PASSWORD
18+
19+
const FIXTURE_OSV_ID = 'osv-test-fixture-reconcile'
20+
21+
interface RangeRow {
22+
introduced_version: string | null
23+
fixed_version: string | null
24+
last_affected: string | null
25+
range_raw: string | null
26+
unaffected_raw: string | null
27+
deleted_at: Date | null
28+
updated_at: Date
29+
}
30+
31+
async function cleanupFixture(qx: QueryExecutor): Promise<void> {
32+
await qx.result(
33+
`
34+
DELETE FROM advisory_affected_ranges
35+
WHERE advisory_package_id IN (
36+
SELECT ap.id FROM advisory_packages ap
37+
JOIN advisories a ON a.id = ap.advisory_id
38+
WHERE a.osv_id = $(osvId)
39+
)
40+
`,
41+
{ osvId: FIXTURE_OSV_ID },
42+
)
43+
await qx.result(
44+
`DELETE FROM advisory_packages WHERE advisory_id IN (SELECT id FROM advisories WHERE osv_id = $(osvId))`,
45+
{ osvId: FIXTURE_OSV_ID },
46+
)
47+
await qx.result(`DELETE FROM advisories WHERE osv_id = $(osvId)`, { osvId: FIXTURE_OSV_ID })
48+
}
49+
50+
async function liveRanges(qx: QueryExecutor, advisoryPackageId: number): Promise<RangeRow[]> {
51+
return qx.select(
52+
`
53+
SELECT introduced_version, fixed_version, last_affected, range_raw, unaffected_raw, deleted_at, updated_at
54+
FROM advisory_affected_ranges
55+
WHERE advisory_package_id = $(advisoryPackageId)
56+
ORDER BY id
57+
`,
58+
{ advisoryPackageId },
59+
)
60+
}
61+
62+
describe.skipIf(!HAVE_DB)('reconcileOsvRanges / supersedeDepsDevRanges — real packages-db', () => {
63+
let qx: QueryExecutor
64+
let advisoryPackageId: number
65+
66+
beforeAll(async () => {
67+
const conn = await getDbConnection({
68+
host: process.env.CROWD_PACKAGES_DB_WRITE_HOST ?? '',
69+
port: parseInt(process.env.CROWD_PACKAGES_DB_PORT ?? '0', 10),
70+
database: process.env.CROWD_PACKAGES_DB_DATABASE ?? '',
71+
user: process.env.CROWD_PACKAGES_DB_USERNAME ?? '',
72+
password: process.env.CROWD_PACKAGES_DB_PASSWORD ?? '',
73+
})
74+
qx = pgpQx(conn)
75+
await cleanupFixture(qx)
76+
77+
const advisory = await qx.selectOne(
78+
`
79+
INSERT INTO advisories (osv_id, source, source_url, aliases, severity, cvss, cvss_source, created_at, updated_at)
80+
VALUES ($(osvId), 'GHSA', NULL, ARRAY[]::text[], 'HIGH', 7.5, 'osv_cvss_v3', NOW(), NOW())
81+
RETURNING id
82+
`,
83+
{ osvId: FIXTURE_OSV_ID },
84+
)
85+
const advisoryPackage = await qx.selectOne(
86+
`
87+
INSERT INTO advisory_packages (advisory_id, package_id, ecosystem, package_name, created_at, updated_at)
88+
VALUES ($(advisoryId), NULL, 'npm', 'reconcile-fixture-pkg', NOW(), NOW())
89+
RETURNING id
90+
`,
91+
{ advisoryId: advisory.id },
92+
)
93+
advisoryPackageId = advisoryPackage.id as number
94+
}, 30_000)
95+
96+
afterAll(async () => {
97+
if (qx) await cleanupFixture(qx)
98+
})
99+
100+
it('inserts the initial OSV range set as live rows', async () => {
101+
await reconcileOsvRanges(qx, advisoryPackageId, [
102+
{
103+
advisoryPackageId,
104+
introducedVersion: '1.0.0',
105+
fixedVersion: '1.2.0',
106+
lastAffected: null,
107+
},
108+
{
109+
advisoryPackageId,
110+
introducedVersion: '2.0.0',
111+
fixedVersion: null,
112+
lastAffected: '2.5.0',
113+
},
114+
])
115+
116+
const rows = await liveRanges(qx, advisoryPackageId)
117+
expect(rows).toHaveLength(2)
118+
expect(rows.every((r) => r.deleted_at === null)).toBe(true)
119+
})
120+
121+
it('leaves an unchanged tuple untouched on a no-op resync (no updated_at bump)', async () => {
122+
const before = await liveRanges(qx, advisoryPackageId)
123+
const beforeUpdatedAt = before.find((r) => r.introduced_version === '1.0.0')?.updated_at
124+
125+
await new Promise((resolve) => setTimeout(resolve, 1100))
126+
await reconcileOsvRanges(qx, advisoryPackageId, [
127+
{
128+
advisoryPackageId,
129+
introducedVersion: '1.0.0',
130+
fixedVersion: '1.2.0',
131+
lastAffected: null,
132+
},
133+
{
134+
advisoryPackageId,
135+
introducedVersion: '2.0.0',
136+
fixedVersion: null,
137+
lastAffected: '2.5.0',
138+
},
139+
])
140+
141+
const after = await liveRanges(qx, advisoryPackageId)
142+
const afterUpdatedAt = after.find((r) => r.introduced_version === '1.0.0')?.updated_at
143+
expect(afterUpdatedAt?.getTime()).toBe(beforeUpdatedAt?.getTime())
144+
})
145+
146+
it('soft-deletes a stale tuple dropped from the new range set', async () => {
147+
await reconcileOsvRanges(qx, advisoryPackageId, [
148+
{
149+
advisoryPackageId,
150+
introducedVersion: '1.0.0',
151+
fixedVersion: '1.2.0',
152+
lastAffected: null,
153+
},
154+
// '2.0.0'..'2.5.0' range dropped — OSV no longer reports it.
155+
])
156+
157+
const rows = await qx.select(
158+
`
159+
SELECT introduced_version, deleted_at
160+
FROM advisory_affected_ranges
161+
WHERE advisory_package_id = $(advisoryPackageId) AND introduced_version = '2.0.0'
162+
`,
163+
{ advisoryPackageId },
164+
)
165+
expect(rows).toHaveLength(1)
166+
expect(rows[0].deleted_at).not.toBeNull()
167+
})
168+
169+
it('revives a tombstoned tuple that reappears in a later sync', async () => {
170+
await reconcileOsvRanges(qx, advisoryPackageId, [
171+
{
172+
advisoryPackageId,
173+
introducedVersion: '1.0.0',
174+
fixedVersion: '1.2.0',
175+
lastAffected: null,
176+
},
177+
{
178+
advisoryPackageId,
179+
introducedVersion: '2.0.0',
180+
fixedVersion: null,
181+
lastAffected: '2.5.0',
182+
},
183+
])
184+
185+
const rows = await qx.select(
186+
`
187+
SELECT deleted_at
188+
FROM advisory_affected_ranges
189+
WHERE advisory_package_id = $(advisoryPackageId) AND introduced_version = '2.0.0'
190+
`,
191+
{ advisoryPackageId },
192+
)
193+
expect(rows).toHaveLength(1)
194+
expect(rows[0].deleted_at).toBeNull()
195+
})
196+
197+
it('supersedes a live deps.dev raw row once OSV owns the package', async () => {
198+
await qx.result(
199+
`
200+
INSERT INTO advisory_affected_ranges
201+
(advisory_package_id, range_raw, unaffected_raw, introduced_version, created_at, updated_at)
202+
VALUES ($(advisoryPackageId), '>=1.0.0 <1.2.0', NULL, NULL, NOW(), NOW())
203+
`,
204+
{ advisoryPackageId },
205+
)
206+
207+
await supersedeDepsDevRanges(qx, advisoryPackageId)
208+
209+
const rows = await qx.select(
210+
`
211+
SELECT deleted_at
212+
FROM advisory_affected_ranges
213+
WHERE advisory_package_id = $(advisoryPackageId) AND range_raw = '>=1.0.0 <1.2.0'
214+
`,
215+
{ advisoryPackageId },
216+
)
217+
expect(rows).toHaveLength(1)
218+
expect(rows[0].deleted_at).not.toBeNull()
219+
})
220+
})

0 commit comments

Comments
 (0)