Skip to content

Commit f2fa311

Browse files
committed
test(packages/provenance): cover findProvenance + getAttestations + getProvenanceDetails + isTrustedPublisher (24 added, 0% → 86% br)
1 parent 547ace3 commit f2fa311

1 file changed

Lines changed: 247 additions & 0 deletions

File tree

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* @file Unit tests for packages/provenance.ts — pure helpers for parsing and
3+
* classifying SLSA attestation data. fetchPackageProvenance() hits the npm
4+
* registry and is exercised via the integration suite; this file targets the
5+
* synchronous parse/classify path used by the trusted-publisher gate.
6+
*/
7+
8+
import { describe, expect, it } from 'vitest'
9+
10+
import {
11+
findProvenance,
12+
getAttestations,
13+
getProvenanceDetails,
14+
isTrustedPublisher,
15+
} from '../../../src/packages/provenance'
16+
17+
describe('packages/provenance — getAttestations', () => {
18+
it('returns [] when input has no attestations field', () => {
19+
expect(getAttestations({})).toEqual([])
20+
})
21+
22+
it('returns [] when attestations is not an array', () => {
23+
expect(getAttestations({ attestations: 'not-an-array' })).toEqual([])
24+
})
25+
26+
it('filters to SLSA v0.2 and v1 predicate types', () => {
27+
const data = {
28+
attestations: [
29+
{ predicateType: 'https://slsa.dev/provenance/v0.2' },
30+
{ predicateType: 'https://slsa.dev/provenance/v1' },
31+
{ predicateType: 'https://example.org/unrelated' },
32+
{ predicateType: 'https://in-toto.io/Statement/v1' },
33+
],
34+
}
35+
const result = getAttestations(data)
36+
expect(result).toHaveLength(2)
37+
})
38+
39+
it('returns [] when attestations is empty array', () => {
40+
expect(getAttestations({ attestations: [] })).toEqual([])
41+
})
42+
})
43+
44+
describe('packages/provenance — findProvenance', () => {
45+
it('returns undefined for an empty list', () => {
46+
expect(findProvenance([])).toBeUndefined()
47+
})
48+
49+
it('returns provenance shape when predicate is directly available', () => {
50+
const att = {
51+
predicate: {
52+
buildDefinition: {
53+
externalParameters: { workflow: { ref: 'refs/heads/main' } },
54+
},
55+
},
56+
}
57+
const result = findProvenance([att]) as {
58+
predicate: unknown
59+
externalParameters: unknown
60+
}
61+
expect(result.predicate).toBe(att.predicate)
62+
expect(result.externalParameters).toEqual({
63+
workflow: { ref: 'refs/heads/main' },
64+
})
65+
})
66+
67+
it('decodes predicate from DSSE envelope payload when not directly present', () => {
68+
const statement = {
69+
predicate: {
70+
buildDefinition: {
71+
externalParameters: { workflow: { ref: 'refs/heads/dsse' } },
72+
},
73+
},
74+
}
75+
const payload = Buffer.from(JSON.stringify(statement), 'utf8').toString(
76+
'base64',
77+
)
78+
const att = { bundle: { dsseEnvelope: { payload } } }
79+
const result = findProvenance([att]) as {
80+
externalParameters: { workflow: { ref: string } }
81+
}
82+
expect(result.externalParameters.workflow.ref).toBe('refs/heads/dsse')
83+
})
84+
85+
it('skips entries with neither predicate nor decodable payload', () => {
86+
const att = { bundle: { dsseEnvelope: { payload: 'not-valid-base64!!!' } } }
87+
expect(findProvenance([att])).toBeUndefined()
88+
})
89+
90+
it('returns undefined when predicate lacks buildDefinition.externalParameters', () => {
91+
const att = { predicate: { buildDefinition: {} } }
92+
expect(findProvenance([att])).toBeUndefined()
93+
})
94+
})
95+
96+
describe('packages/provenance — getProvenanceDetails', () => {
97+
it('returns undefined when no SLSA attestations present', () => {
98+
expect(getProvenanceDetails({ attestations: [] })).toBeUndefined()
99+
})
100+
101+
it('returns { level: "attested" } when attestations exist but no parseable provenance', () => {
102+
const data = {
103+
attestations: [
104+
{
105+
predicateType: 'https://slsa.dev/provenance/v0.2',
106+
predicate: { buildDefinition: {} },
107+
},
108+
],
109+
}
110+
expect(getProvenanceDetails(data)).toEqual({ level: 'attested' })
111+
})
112+
113+
it('extracts SLSA v1 nested workflow shape into details', () => {
114+
const data = {
115+
attestations: [
116+
{
117+
predicateType: 'https://slsa.dev/provenance/v1',
118+
predicate: {
119+
buildDefinition: {
120+
buildType: 'https://actions.github.io/buildtypes/workflow/v1',
121+
externalParameters: {
122+
workflow: {
123+
ref: 'refs/heads/main',
124+
repository: 'owner/repo',
125+
},
126+
context: 'https://github.com/owner/repo/actions/runs/1',
127+
ref: 'refs/heads/main',
128+
sha: 'abc123',
129+
run_id: '1',
130+
},
131+
},
132+
},
133+
},
134+
],
135+
}
136+
const details = getProvenanceDetails(data) as {
137+
level: string
138+
commitSha: string
139+
repository: string
140+
workflowRunId: string
141+
}
142+
expect(details.level).toBe('trusted')
143+
expect(details.commitSha).toBe('abc123')
144+
expect(details.repository).toBe('owner/repo')
145+
expect(details.workflowRunId).toBe('1')
146+
})
147+
148+
it('marks as "trusted" when repository hostname is gitlab.com', () => {
149+
const data = {
150+
attestations: [
151+
{
152+
predicateType: 'https://slsa.dev/provenance/v0.2',
153+
predicate: {
154+
buildDefinition: {
155+
externalParameters: {
156+
workflow_ref: 'https://gitlab.com/group/proj/.gitlab-ci.yml@main',
157+
},
158+
},
159+
},
160+
},
161+
],
162+
}
163+
const details = getProvenanceDetails(data) as { level: string }
164+
expect(details.level).toBe('trusted')
165+
})
166+
167+
it('marks as "attested" (not trusted) when no recognized publisher', () => {
168+
const data = {
169+
attestations: [
170+
{
171+
predicateType: 'https://slsa.dev/provenance/v0.2',
172+
predicate: {
173+
buildDefinition: {
174+
externalParameters: {
175+
workflow_ref: 'https://example.org/repo/ci.yml@main',
176+
},
177+
},
178+
},
179+
},
180+
],
181+
}
182+
const details = getProvenanceDetails(data) as { level: string }
183+
expect(details.level).toBe('attested')
184+
})
185+
})
186+
187+
describe('packages/provenance — isTrustedPublisher', () => {
188+
it('returns false for non-string or empty input', () => {
189+
expect(isTrustedPublisher(null)).toBe(false)
190+
expect(isTrustedPublisher(undefined)).toBe(false)
191+
expect(isTrustedPublisher(42)).toBe(false)
192+
expect(isTrustedPublisher('')).toBe(false)
193+
})
194+
195+
it('returns true for github.com URLs', () => {
196+
expect(isTrustedPublisher('https://github.com/o/r')).toBe(true)
197+
expect(isTrustedPublisher('https://api.github.com/repos/o/r')).toBe(true)
198+
})
199+
200+
it('returns true for gitlab.com URLs', () => {
201+
expect(isTrustedPublisher('https://gitlab.com/g/p')).toBe(true)
202+
expect(isTrustedPublisher('https://nested.gitlab.com/path')).toBe(true)
203+
})
204+
205+
it('handles workflow @-suffix by splitting on @', () => {
206+
expect(
207+
isTrustedPublisher(
208+
'https://github.com/o/r/.github/workflows/ci.yml@refs/heads/main',
209+
),
210+
).toBe(true)
211+
})
212+
213+
it('handles @-suffix where first part is also not a URL', () => {
214+
// "@" is present, but the value before "@" isn't a valid URL either.
215+
// The fall-through then tries `https://` prefix and matches.
216+
expect(isTrustedPublisher('github.com/o/r@refs/heads/main')).toBe(true)
217+
})
218+
219+
it('handles @-suffix where first part is empty after split', () => {
220+
// Edge: split returns ['', ...] — the inner if(firstPart) guard fires.
221+
expect(isTrustedPublisher('@github.com/o')).toBe(true)
222+
})
223+
224+
it('returns true for bare hostnames (synthetic https:// prefix)', () => {
225+
expect(isTrustedPublisher('github.com/o/r')).toBe(true)
226+
expect(isTrustedPublisher('gitlab.com/g/p')).toBe(true)
227+
})
228+
229+
it('falls back to substring match for strings without a hostname', () => {
230+
// Empty / whitespace-stripped strings still pass the typeof guard
231+
// but won't yield a hostname from parseUrl. URL "http://" parses
232+
// with an empty hostname; "http:" alone is also unparseable in node URL.
233+
// Use a value that contains the substring but no URL form: a plain
234+
// identifier that survives the hostname check.
235+
expect(isTrustedPublisher('runner:github')).toBe(true)
236+
expect(isTrustedPublisher('runner:gitlab')).toBe(true)
237+
})
238+
239+
it('returns false for unrelated hostnames', () => {
240+
expect(isTrustedPublisher('https://example.org/foo')).toBe(false)
241+
expect(isTrustedPublisher('https://bitbucket.org/o/r')).toBe(false)
242+
})
243+
244+
it('returns false for malformed-but-known-bad strings', () => {
245+
expect(isTrustedPublisher('not a url not even close')).toBe(false)
246+
})
247+
})

0 commit comments

Comments
 (0)