forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import { detectPublishSecurityDowngradeForVersion } from '../../../../app/utils/publish-security'
3
4describe('detectPublishSecurityDowngradeForVersion', () => {
5 const versions = [
6 {
7 version: '1.0.0',
8 time: '2026-01-01T00:00:00.000Z',
9 hasProvenance: true,
10 },
11 {
12 version: '1.0.1',
13 time: '2026-01-02T00:00:00.000Z',
14 hasProvenance: false,
15 },
16 {
17 version: '1.0.2',
18 time: '2026-01-03T00:00:00.000Z',
19 hasProvenance: true,
20 },
21 ]
22
23 it('does not flag trusted viewed version (1.0.2)', () => {
24 const result = detectPublishSecurityDowngradeForVersion(versions, '1.0.2')
25 expect(result).toBeNull()
26 })
27
28 it('flags downgraded viewed version (1.0.1)', () => {
29 const result = detectPublishSecurityDowngradeForVersion(versions, '1.0.1')
30 expect(result).toEqual({
31 downgradedVersion: '1.0.1',
32 downgradedPublishedAt: '2026-01-02T00:00:00.000Z',
33 downgradedTrustLevel: 'none',
34 trustedVersion: '1.0.0',
35 trustedPublishedAt: '2026-01-01T00:00:00.000Z',
36 trustedTrustLevel: 'provenance',
37 })
38 })
39
40 it('flags trust downgrade from trustedPublisher to provenance', () => {
41 const result = detectPublishSecurityDowngradeForVersion(
42 [
43 {
44 version: '1.0.0',
45 time: '2026-01-01T00:00:00.000Z',
46 hasProvenance: true,
47 trustLevel: 'trustedPublisher',
48 },
49 {
50 version: '1.0.1',
51 time: '2026-01-02T00:00:00.000Z',
52 hasProvenance: true,
53 trustLevel: 'provenance',
54 },
55 ],
56 '1.0.1',
57 )
58
59 expect(result).toEqual({
60 downgradedVersion: '1.0.1',
61 downgradedPublishedAt: '2026-01-02T00:00:00.000Z',
62 downgradedTrustLevel: 'provenance',
63 trustedVersion: '1.0.0',
64 trustedPublishedAt: '2026-01-01T00:00:00.000Z',
65 trustedTrustLevel: 'trustedPublisher',
66 })
67 })
68
69 it('does not flag upgrade from provenance to trustedPublisher', () => {
70 const result = detectPublishSecurityDowngradeForVersion(
71 [
72 {
73 version: '1.0.0',
74 time: '2026-01-01T00:00:00.000Z',
75 hasProvenance: true,
76 trustLevel: 'provenance',
77 },
78 {
79 version: '1.0.1',
80 time: '2026-01-02T00:00:00.000Z',
81 hasProvenance: true,
82 trustLevel: 'trustedPublisher',
83 },
84 ],
85 '1.0.1',
86 )
87
88 expect(result).toBeNull()
89 })
90
91 it('flags ongoing downgraded versions until an upgrade happens', () => {
92 const versions = [
93 {
94 version: '2.1.0',
95 time: '2026-01-01T00:00:00.000Z',
96 hasProvenance: true,
97 trustLevel: 'provenance' as const,
98 },
99 {
100 version: '2.1.1',
101 time: '2026-01-02T00:00:00.000Z',
102 hasProvenance: false,
103 trustLevel: 'none' as const,
104 },
105 {
106 version: '2.2.0',
107 time: '2026-01-03T00:00:00.000Z',
108 hasProvenance: false,
109 trustLevel: 'none' as const,
110 },
111 {
112 version: '2.3.0',
113 time: '2026-01-04T00:00:00.000Z',
114 hasProvenance: false,
115 trustLevel: 'none' as const,
116 },
117 {
118 version: '2.4.0',
119 time: '2026-01-05T00:00:00.000Z',
120 hasProvenance: true,
121 trustLevel: 'provenance' as const,
122 },
123 ]
124
125 expect(detectPublishSecurityDowngradeForVersion(versions, '2.1.1')?.trustedVersion).toBe(
126 '2.1.0',
127 )
128 expect(detectPublishSecurityDowngradeForVersion(versions, '2.2.0')?.trustedVersion).toBe(
129 '2.1.0',
130 )
131 expect(detectPublishSecurityDowngradeForVersion(versions, '2.3.0')?.trustedVersion).toBe(
132 '2.1.0',
133 )
134 expect(detectPublishSecurityDowngradeForVersion(versions, '2.4.0')).toBeNull()
135 })
136
137 it('skips deprecated versions when selecting trustedVersion', () => {
138 const result = detectPublishSecurityDowngradeForVersion(
139 [
140 {
141 version: '1.0.0',
142 time: '2026-01-01T00:00:00.000Z',
143 hasProvenance: true,
144 trustLevel: 'provenance',
145 },
146 {
147 version: '1.0.1',
148 time: '2026-01-02T00:00:00.000Z',
149 hasProvenance: true,
150 trustLevel: 'provenance',
151 deprecated: 'Use 1.0.2 instead',
152 },
153 {
154 version: '1.0.2',
155 time: '2026-01-03T00:00:00.000Z',
156 hasProvenance: false,
157 trustLevel: 'none',
158 },
159 ],
160 '1.0.2',
161 )
162
163 // Should recommend 1.0.0 (not 1.0.1 which is deprecated)
164 expect(result?.trustedVersion).toBe('1.0.0')
165 })
166
167 it('returns null when all older trusted versions are deprecated', () => {
168 const result = detectPublishSecurityDowngradeForVersion(
169 [
170 {
171 version: '1.0.0',
172 time: '2026-01-01T00:00:00.000Z',
173 hasProvenance: true,
174 trustLevel: 'provenance',
175 deprecated: 'Deprecated',
176 },
177 {
178 version: '1.0.1',
179 time: '2026-01-02T00:00:00.000Z',
180 hasProvenance: false,
181 trustLevel: 'none',
182 },
183 ],
184 '1.0.1',
185 )
186
187 expect(result).toBeNull()
188 })
189
190 it('detects cross-major downgrade but does not recommend a version', () => {
191 const result = detectPublishSecurityDowngradeForVersion(
192 [
193 {
194 version: '1.0.0',
195 time: '2026-01-01T00:00:00.000Z',
196 hasProvenance: true,
197 trustLevel: 'provenance',
198 },
199 {
200 version: '2.0.0',
201 time: '2026-01-02T00:00:00.000Z',
202 hasProvenance: false,
203 trustLevel: 'none',
204 },
205 ],
206 '2.0.0',
207 )
208
209 // Downgrade is detected (v1.0.0 was trusted, v2.0.0 is not)
210 expect(result).not.toBeNull()
211 expect(result?.downgradedVersion).toBe('2.0.0')
212 // But no trustedVersion recommendation since v1.0.0 is a different major
213 expect(result?.trustedVersion).toBeUndefined()
214 })
215
216 it('recommends same-major trusted version when cross-major exists', () => {
217 const result = detectPublishSecurityDowngradeForVersion(
218 [
219 {
220 version: '1.0.0',
221 time: '2026-01-01T00:00:00.000Z',
222 hasProvenance: true,
223 trustLevel: 'provenance',
224 },
225 {
226 version: '2.0.0',
227 time: '2026-01-02T00:00:00.000Z',
228 hasProvenance: true,
229 trustLevel: 'provenance',
230 },
231 {
232 version: '2.1.0',
233 time: '2026-01-03T00:00:00.000Z',
234 hasProvenance: false,
235 trustLevel: 'none',
236 },
237 ],
238 '2.1.0',
239 )
240
241 // Should recommend 2.0.0 (same major), not 1.0.0
242 expect(result?.trustedVersion).toBe('2.0.0')
243 })
244
245 it('uses provenance rank (not trustedPublisher) for hasProvenance fallback without trustLevel', () => {
246 // When trustLevel is absent, hasProvenance: true should map to provenance rank,
247 // not trustedPublisher rank. This means a version with only hasProvenance: true
248 // should be considered a downgrade from trustedPublisher.
249 const result = detectPublishSecurityDowngradeForVersion(
250 [
251 {
252 version: '1.0.0',
253 time: '2026-01-01T00:00:00.000Z',
254 hasProvenance: true,
255 trustLevel: 'trustedPublisher',
256 },
257 {
258 version: '1.0.1',
259 time: '2026-01-02T00:00:00.000Z',
260 hasProvenance: true,
261 // no trustLevel — fallback path maps to provenance
262 },
263 ],
264 '1.0.1',
265 )
266
267 // hasProvenance fallback maps to provenance (rank 1), trustedPublisher is rank 2, so this is a downgrade
268 expect(result).toEqual({
269 downgradedVersion: '1.0.1',
270 downgradedPublishedAt: '2026-01-02T00:00:00.000Z',
271 downgradedTrustLevel: 'provenance',
272 trustedVersion: '1.0.0',
273 trustedPublishedAt: '2026-01-01T00:00:00.000Z',
274 trustedTrustLevel: 'trustedPublisher',
275 })
276 })
277
278 it('does not flag hasProvenance fallback against provenance trustLevel', () => {
279 // When trustLevel is absent, hasProvenance: true maps to provenance rank.
280 // An explicit provenance trustLevel is the same rank, so no downgrade.
281 const result = detectPublishSecurityDowngradeForVersion(
282 [
283 {
284 version: '1.0.0',
285 time: '2026-01-01T00:00:00.000Z',
286 hasProvenance: true,
287 // no trustLevel — fallback path maps to provenance
288 },
289 {
290 version: '1.0.1',
291 time: '2026-01-02T00:00:00.000Z',
292 hasProvenance: true,
293 trustLevel: 'provenance',
294 },
295 ],
296 '1.0.1',
297 )
298
299 // Both are provenance rank, so no downgrade
300 expect(result).toBeNull()
301 })
302})