Coverage for tests / unit / tools / cargo_audit / test_cargo_audit_plugin.py: 100%

74 statements  

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

1"""Unit tests for cargo-audit plugin.""" 

2 

3from __future__ import annotations 

4 

5from pathlib import Path 

6from subprocess import TimeoutExpired 

7from unittest.mock import patch 

8 

9import pytest 

10from assertpy import assert_that 

11 

12from lintro.enums.tool_type import ToolType 

13from lintro.tools.definitions.cargo_audit import ( 

14 CARGO_AUDIT_DEFAULT_TIMEOUT, 

15 CargoAuditPlugin, 

16) 

17 

18 

19@pytest.fixture 

20def cargo_audit_plugin() -> CargoAuditPlugin: 

21 """Provide a CargoAuditPlugin instance for testing. 

22 

23 Returns: 

24 A CargoAuditPlugin instance. 

25 """ 

26 return CargoAuditPlugin() 

27 

28 

29def test_definition_name(cargo_audit_plugin: CargoAuditPlugin) -> None: 

30 """Verify the tool name. 

31 

32 Args: 

33 cargo_audit_plugin: The plugin instance. 

34 """ 

35 assert_that(cargo_audit_plugin.definition.name).is_equal_to("cargo_audit") 

36 

37 

38def test_definition_can_fix(cargo_audit_plugin: CargoAuditPlugin) -> None: 

39 """Verify the tool cannot fix issues. 

40 

41 Args: 

42 cargo_audit_plugin: The plugin instance. 

43 """ 

44 assert_that(cargo_audit_plugin.definition.can_fix).is_false() 

45 

46 

47def test_definition_tool_type(cargo_audit_plugin: CargoAuditPlugin) -> None: 

48 """Verify the tool type is SECURITY. 

49 

50 Args: 

51 cargo_audit_plugin: The plugin instance. 

52 """ 

53 assert_that(cargo_audit_plugin.definition.tool_type).is_equal_to(ToolType.SECURITY) 

54 

55 

56def test_definition_file_patterns(cargo_audit_plugin: CargoAuditPlugin) -> None: 

57 """Verify the file patterns. 

58 

59 Args: 

60 cargo_audit_plugin: The plugin instance. 

61 """ 

62 patterns = cargo_audit_plugin.definition.file_patterns 

63 assert_that(patterns).contains("Cargo.lock") 

64 

65 

66def test_definition_priority(cargo_audit_plugin: CargoAuditPlugin) -> None: 

67 """Verify the priority is 95. 

68 

69 Args: 

70 cargo_audit_plugin: The plugin instance. 

71 """ 

72 assert_that(cargo_audit_plugin.definition.priority).is_equal_to(95) 

73 

74 

75def test_definition_timeout(cargo_audit_plugin: CargoAuditPlugin) -> None: 

76 """Verify the default timeout. 

77 

78 Args: 

79 cargo_audit_plugin: The plugin instance. 

80 """ 

81 assert_that(cargo_audit_plugin.definition.default_timeout).is_equal_to( 

82 CARGO_AUDIT_DEFAULT_TIMEOUT, 

83 ) 

84 

85 

86def test_definition_native_configs(cargo_audit_plugin: CargoAuditPlugin) -> None: 

87 """Verify the native config files. 

88 

89 Args: 

90 cargo_audit_plugin: The plugin instance. 

91 """ 

92 configs = cargo_audit_plugin.definition.native_configs 

93 assert_that(configs).contains(".cargo/audit.toml") 

94 

95 

96def test_set_options_timeout(cargo_audit_plugin: CargoAuditPlugin) -> None: 

97 """Verify timeout option can be set. 

98 

99 Args: 

100 cargo_audit_plugin: The plugin instance. 

101 """ 

102 cargo_audit_plugin.set_options(timeout=180) 

103 assert_that(cargo_audit_plugin.options.get("timeout")).is_equal_to(180) 

104 

105 

106def test_set_options_invalid_timeout(cargo_audit_plugin: CargoAuditPlugin) -> None: 

107 """Verify negative integer timeout raises ValueError. 

108 

109 Args: 

110 cargo_audit_plugin: The plugin instance. 

111 """ 

112 with pytest.raises(ValueError, match="non-negative"): 

113 cargo_audit_plugin.set_options(timeout=-1) 

114 

115 

116def test_set_options_negative_float_timeout( 

117 cargo_audit_plugin: CargoAuditPlugin, 

118) -> None: 

119 """Verify negative float timeout raises ValueError. 

120 

121 Args: 

122 cargo_audit_plugin: The plugin instance. 

123 """ 

124 with pytest.raises(ValueError, match="non-negative"): 

125 cargo_audit_plugin.set_options(timeout=-1.5) 

126 

127 

128def test_set_options_non_numeric_timeout( 

129 cargo_audit_plugin: CargoAuditPlugin, 

130) -> None: 

131 """Verify non-numeric timeout raises ValueError. 

132 

133 Args: 

134 cargo_audit_plugin: The plugin instance. 

135 """ 

136 with pytest.raises(ValueError, match="must be a number"): 

137 cargo_audit_plugin.set_options(timeout="invalid") 

138 

139 

140def test_fix_raises_not_implemented(cargo_audit_plugin: CargoAuditPlugin) -> None: 

141 """Verify fix raises NotImplementedError. 

142 

143 Args: 

144 cargo_audit_plugin: The plugin instance. 

145 """ 

146 with pytest.raises(NotImplementedError) as exc_info: 

147 cargo_audit_plugin.fix(["Cargo.lock"], {}) 

148 assert_that(str(exc_info.value)).contains("cannot automatically fix") 

149 

150 

151def test_check_no_cargo_lock( 

152 cargo_audit_plugin: CargoAuditPlugin, 

153 tmp_path: Path, 

154) -> None: 

155 """Check skips gracefully when no Cargo.lock found. 

156 

157 Args: 

158 cargo_audit_plugin: The plugin instance. 

159 tmp_path: Temporary directory path. 

160 """ 

161 # Pass the directory itself, not a file 

162 # This simulates a directory without Cargo.lock 

163 

164 with patch( 

165 "lintro.plugins.execution_preparation.verify_tool_version", 

166 return_value=None, 

167 ): 

168 result = cargo_audit_plugin.check([str(tmp_path)], {}) 

169 

170 # Either no files found or no Cargo.lock found message 

171 assert_that(result.success).is_true() 

172 assert_that(result.issues_count).is_equal_to(0) 

173 

174 

175def test_check_no_vulnerabilities( 

176 cargo_audit_plugin: CargoAuditPlugin, 

177 tmp_path: Path, 

178) -> None: 

179 """Check returns success when no vulnerabilities found. 

180 

181 Args: 

182 cargo_audit_plugin: The plugin instance. 

183 tmp_path: Temporary directory path. 

184 """ 

185 cargo_lock = tmp_path / "Cargo.lock" 

186 cargo_lock.write_text('[[package]]\nname = "test"\nversion = "1.0.0"') 

187 

188 mock_output = """{ 

189 "vulnerabilities": { 

190 "count": 0, 

191 "list": [] 

192 } 

193 }""" 

194 

195 with patch( 

196 "lintro.plugins.execution_preparation.verify_tool_version", 

197 return_value=None, 

198 ): 

199 with patch.object( 

200 cargo_audit_plugin, 

201 "_run_subprocess", 

202 return_value=(True, mock_output), 

203 ): 

204 result = cargo_audit_plugin.check([str(cargo_lock)], {}) 

205 

206 assert_that(result.success).is_true() 

207 assert_that(result.issues_count).is_equal_to(0) 

208 

209 

210def test_check_with_vulnerabilities( 

211 cargo_audit_plugin: CargoAuditPlugin, 

212 tmp_path: Path, 

213) -> None: 

214 """Check returns issues when vulnerabilities found. 

215 

216 Args: 

217 cargo_audit_plugin: The plugin instance. 

218 tmp_path: Temporary directory path. 

219 """ 

220 cargo_lock = tmp_path / "Cargo.lock" 

221 cargo_lock.write_text('[[package]]\nname = "test"\nversion = "1.0.0"') 

222 

223 mock_output = """{ 

224 "vulnerabilities": { 

225 "count": 1, 

226 "list": [ 

227 { 

228 "advisory": { 

229 "id": "RUSTSEC-2021-0001", 

230 "title": "Test vulnerability", 

231 "severity": "HIGH" 

232 }, 

233 "package": { 

234 "name": "vulnerable-crate", 

235 "version": "1.0.0" 

236 } 

237 } 

238 ] 

239 } 

240 }""" 

241 

242 with patch( 

243 "lintro.plugins.execution_preparation.verify_tool_version", 

244 return_value=None, 

245 ): 

246 with patch.object( 

247 cargo_audit_plugin, 

248 "_run_subprocess", 

249 return_value=(False, mock_output), 

250 ): 

251 result = cargo_audit_plugin.check([str(cargo_lock)], {}) 

252 

253 assert_that(result.success).is_false() 

254 assert_that(result.issues_count).is_greater_than(0) 

255 

256 

257def test_check_timeout( 

258 cargo_audit_plugin: CargoAuditPlugin, 

259 tmp_path: Path, 

260) -> None: 

261 """Check handles timeout correctly. 

262 

263 Args: 

264 cargo_audit_plugin: The plugin instance. 

265 tmp_path: Temporary directory path. 

266 """ 

267 cargo_lock = tmp_path / "Cargo.lock" 

268 cargo_lock.write_text('[[package]]\nname = "test"\nversion = "1.0.0"') 

269 

270 with patch( 

271 "lintro.plugins.execution_preparation.verify_tool_version", 

272 return_value=None, 

273 ): 

274 with patch.object( 

275 cargo_audit_plugin, 

276 "_run_subprocess", 

277 side_effect=TimeoutExpired( 

278 cmd=["cargo", "audit"], 

279 timeout=120, 

280 ), 

281 ): 

282 result = cargo_audit_plugin.check([str(cargo_lock)], {}) 

283 

284 assert_that(result.success).is_false() 

285 assert_that(result.output).contains("timed out")