[READ-ONLY] a fast, modern browser for the npm registry
at main 302 lines 8.7 kB view raw
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})