Coverage for tests / unit / parsers / test_osv_suppression_parser.py: 100%

87 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Unit tests for OSV-Scanner suppression parser.""" 

2 

3from __future__ import annotations 

4 

5from datetime import date 

6from pathlib import Path 

7 

8import pytest 

9from assertpy import assert_that 

10 

11from lintro.parsers.osv_scanner.suppression_models import SuppressionEntry 

12from lintro.parsers.osv_scanner.suppression_parser import ( 

13 classify_suppressions, 

14 parse_suppressions, 

15) 

16from lintro.parsers.osv_scanner.suppression_status import SuppressionStatus 

17 

18# ============================================================================= 

19# Tests for parse_suppressions 

20# ============================================================================= 

21 

22 

23def test_parse_well_formed_toml(tmp_path: Path) -> None: 

24 """Parse a valid .osv-scanner.toml with multiple entries.""" 

25 toml_file = tmp_path / ".osv-scanner.toml" 

26 toml_file.write_text( 

27 "[[IgnoredVulns]]\n" 

28 'id = "GHSA-1111-aaaa-bbbb"\n' 

29 "ignoreUntil = 2026-12-31\n" 

30 'reason = "Low risk transitive dep"\n' 

31 "\n" 

32 "[[IgnoredVulns]]\n" 

33 'id = "CVE-2024-99999"\n' 

34 "ignoreUntil = 2026-06-15\n" 

35 'reason = "No fix available yet"\n', 

36 ) 

37 

38 entries = parse_suppressions(toml_file) 

39 

40 assert_that(entries).is_length(2) 

41 assert_that(entries[0].id).is_equal_to("GHSA-1111-aaaa-bbbb") 

42 assert_that(entries[0].ignore_until).is_equal_to(date(2026, 12, 31)) 

43 assert_that(entries[0].reason).is_equal_to("Low risk transitive dep") 

44 assert_that(entries[1].id).is_equal_to("CVE-2024-99999") 

45 assert_that(entries[1].ignore_until).is_equal_to(date(2026, 6, 15)) 

46 

47 

48def test_parse_missing_file(tmp_path: Path) -> None: 

49 """Return empty list for nonexistent file.""" 

50 entries = parse_suppressions(tmp_path / "nonexistent.toml") 

51 assert_that(entries).is_equal_to([]) 

52 

53 

54def test_parse_empty_file(tmp_path: Path) -> None: 

55 """Return empty list for empty TOML file.""" 

56 toml_file = tmp_path / ".osv-scanner.toml" 

57 toml_file.write_text("") 

58 

59 entries = parse_suppressions(toml_file) 

60 assert_that(entries).is_equal_to([]) 

61 

62 

63def test_parse_no_ignored_vulns(tmp_path: Path) -> None: 

64 """Return empty list when TOML has no IgnoredVulns key.""" 

65 toml_file = tmp_path / ".osv-scanner.toml" 

66 toml_file.write_text('[SomeOtherSection]\nkey = "value"\n') 

67 

68 entries = parse_suppressions(toml_file) 

69 assert_that(entries).is_equal_to([]) 

70 

71 

72def test_parse_skips_entry_without_id(tmp_path: Path) -> None: 

73 """Skip entries that have no id field.""" 

74 toml_file = tmp_path / ".osv-scanner.toml" 

75 toml_file.write_text( 

76 "[[IgnoredVulns]]\n" "ignoreUntil = 2026-12-31\n" 'reason = "Missing id"\n', 

77 ) 

78 

79 entries = parse_suppressions(toml_file) 

80 assert_that(entries).is_equal_to([]) 

81 

82 

83def test_parse_skips_entry_without_ignore_until(tmp_path: Path) -> None: 

84 """Skip entries that have no ignoreUntil field.""" 

85 toml_file = tmp_path / ".osv-scanner.toml" 

86 toml_file.write_text( 

87 "[[IgnoredVulns]]\n" 'id = "GHSA-1111-aaaa-bbbb"\n' 'reason = "Missing date"\n', 

88 ) 

89 

90 entries = parse_suppressions(toml_file) 

91 assert_that(entries).is_equal_to([]) 

92 

93 

94def test_parse_missing_reason_defaults_empty(tmp_path: Path) -> None: 

95 """Missing reason field defaults to empty string.""" 

96 toml_file = tmp_path / ".osv-scanner.toml" 

97 toml_file.write_text( 

98 "[[IgnoredVulns]]\n" 

99 'id = "GHSA-1111-aaaa-bbbb"\n' 

100 "ignoreUntil = 2026-12-31\n", 

101 ) 

102 

103 entries = parse_suppressions(toml_file) 

104 assert_that(entries).is_length(1) 

105 assert_that(entries[0].reason).is_equal_to("") 

106 

107 

108def test_parse_invalid_toml(tmp_path: Path) -> None: 

109 """Return empty list for malformed TOML.""" 

110 toml_file = tmp_path / ".osv-scanner.toml" 

111 toml_file.write_text("this is not valid toml [[[") 

112 

113 entries = parse_suppressions(toml_file) 

114 assert_that(entries).is_equal_to([]) 

115 

116 

117# ============================================================================= 

118# Tests for classify_suppressions 

119# ============================================================================= 

120 

121 

122def test_classify_expired() -> None: 

123 """Entry past ignoreUntil is EXPIRED.""" 

124 entry = SuppressionEntry( 

125 id="GHSA-expired", 

126 ignore_until=date(2025, 1, 1), 

127 reason="Old suppression", 

128 ) 

129 

130 classified = classify_suppressions( 

131 [entry], 

132 probe_vuln_ids={"GHSA-expired"}, 

133 today=date(2026, 3, 26), 

134 ) 

135 

136 assert_that(classified).is_length(1) 

137 assert_that(classified[0].status).is_equal_to(SuppressionStatus.EXPIRED) 

138 

139 

140def test_classify_active() -> None: 

141 """Entry still within date and found in probe is ACTIVE.""" 

142 entry = SuppressionEntry( 

143 id="GHSA-active", 

144 ignore_until=date(2027, 12, 31), 

145 reason="Still present", 

146 ) 

147 

148 classified = classify_suppressions( 

149 [entry], 

150 probe_vuln_ids={"GHSA-active", "GHSA-other"}, 

151 today=date(2026, 3, 26), 

152 ) 

153 

154 assert_that(classified).is_length(1) 

155 assert_that(classified[0].status).is_equal_to(SuppressionStatus.ACTIVE) 

156 

157 

158def test_classify_stale() -> None: 

159 """Entry within date but NOT found in probe is STALE.""" 

160 entry = SuppressionEntry( 

161 id="GHSA-stale", 

162 ignore_until=date(2027, 12, 31), 

163 reason="Fixed upstream", 

164 ) 

165 

166 classified = classify_suppressions( 

167 [entry], 

168 probe_vuln_ids={"GHSA-other"}, 

169 today=date(2026, 3, 26), 

170 ) 

171 

172 assert_that(classified).is_length(1) 

173 assert_that(classified[0].status).is_equal_to(SuppressionStatus.STALE) 

174 

175 

176def test_classify_boundary_today_equals_ignore_until() -> None: 

177 """Entry with ignoreUntil == today is NOT expired (still active/stale).""" 

178 entry = SuppressionEntry( 

179 id="GHSA-boundary", 

180 ignore_until=date(2026, 3, 26), 

181 reason="Boundary case", 

182 ) 

183 

184 # In probe (active, not expired) 

185 classified = classify_suppressions( 

186 [entry], 

187 probe_vuln_ids={"GHSA-boundary"}, 

188 today=date(2026, 3, 26), 

189 ) 

190 

191 assert_that(classified[0].status).is_equal_to(SuppressionStatus.ACTIVE) 

192 

193 

194def test_classify_empty_entries() -> None: 

195 """Empty entries list returns empty classified list.""" 

196 classified = classify_suppressions( 

197 [], 

198 probe_vuln_ids={"GHSA-something"}, 

199 today=date(2026, 3, 26), 

200 ) 

201 

202 assert_that(classified).is_equal_to([]) 

203 

204 

205def test_classify_multiple_entries() -> None: 

206 """Classify a mix of active, stale, and expired entries.""" 

207 entries = [ 

208 SuppressionEntry( 

209 id="GHSA-active", 

210 ignore_until=date(2027, 1, 1), 

211 reason="Still present", 

212 ), 

213 SuppressionEntry( 

214 id="GHSA-stale", 

215 ignore_until=date(2027, 1, 1), 

216 reason="Fixed upstream", 

217 ), 

218 SuppressionEntry( 

219 id="GHSA-expired", 

220 ignore_until=date(2025, 1, 1), 

221 reason="Past date", 

222 ), 

223 ] 

224 

225 classified = classify_suppressions( 

226 entries, 

227 probe_vuln_ids={"GHSA-active"}, 

228 today=date(2026, 3, 26), 

229 ) 

230 

231 assert_that(classified).is_length(3) 

232 assert_that(classified[0].status).is_equal_to(SuppressionStatus.ACTIVE) 

233 assert_that(classified[1].status).is_equal_to(SuppressionStatus.STALE) 

234 assert_that(classified[2].status).is_equal_to(SuppressionStatus.EXPIRED) 

235 

236 

237@pytest.mark.parametrize( 

238 ("ignore_until", "today", "in_probe", "expected"), 

239 [ 

240 pytest.param( 

241 date(2025, 1, 1), 

242 date(2026, 1, 1), 

243 True, 

244 SuppressionStatus.EXPIRED, 

245 id="expired_even_if_in_probe", 

246 ), 

247 pytest.param( 

248 date(2025, 1, 1), 

249 date(2026, 1, 1), 

250 False, 

251 SuppressionStatus.EXPIRED, 

252 id="expired_and_not_in_probe", 

253 ), 

254 pytest.param( 

255 date(2027, 1, 1), 

256 date(2026, 1, 1), 

257 True, 

258 SuppressionStatus.ACTIVE, 

259 id="future_and_in_probe", 

260 ), 

261 pytest.param( 

262 date(2027, 1, 1), 

263 date(2026, 1, 1), 

264 False, 

265 SuppressionStatus.STALE, 

266 id="future_and_not_in_probe", 

267 ), 

268 ], 

269) 

270def test_classify_parametrized( 

271 ignore_until: date, 

272 today: date, 

273 in_probe: bool, 

274 expected: SuppressionStatus, 

275) -> None: 

276 """Parametrized classification covering all state combinations.""" 

277 entry = SuppressionEntry(id="GHSA-test", ignore_until=ignore_until, reason="test") 

278 probe_ids = {"GHSA-test"} if in_probe else set() 

279 

280 classified = classify_suppressions([entry], probe_ids, today=today) 

281 

282 assert_that(classified[0].status).is_equal_to(expected)