Coverage for tests / unit / ai / test_secrets.py: 100%

47 statements  

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

1"""Tests for AI secrets scanning and redaction.""" 

2 

3from __future__ import annotations 

4 

5import pytest 

6from assertpy import assert_that 

7 

8from lintro.ai.secrets import redact_secrets, scan_for_secrets 

9 

10# -- scan_for_secrets: no secrets --------------------------------------------- 

11 

12 

13def test_no_secrets_in_clean_code() -> None: 

14 """Clean code with no secrets returns empty list.""" 

15 text = "def hello():\n return 'world'\n" 

16 assert_that(scan_for_secrets(text)).is_empty() 

17 

18 

19def test_empty_string_no_secrets() -> None: 

20 """Empty string returns no secrets.""" 

21 assert_that(scan_for_secrets("")).is_empty() 

22 

23 

24def test_normal_variable_assignments() -> None: 

25 """Normal variable assignments are not flagged.""" 

26 text = "name = 'Alice'\ncount = 42\npath = '/usr/local'\n" 

27 assert_that(scan_for_secrets(text)).is_empty() 

28 

29 

30# -- scan_for_secrets: pattern detection (parametrized) ----------------------- 

31 

32 

33@pytest.mark.parametrize( 

34 ("description", "text", "expected_pattern"), 

35 [ 

36 ( 

37 "api_key assignment", 

38 "api_key = 'ABCDEFGHIJKLMNOPQRST1234567890'\n", 

39 "api", 

40 ), 

41 ( 

42 "apikey no separator", 

43 "apikey = 'ABCDEFGHIJKLMNOPQRST1234567890'\n", 

44 "api", 

45 ), 

46 ( 

47 "api-key header", 

48 "api-key: ABCDEFGHIJKLMNOPQRST1234567890\n", 

49 "api", 

50 ), 

51 ( 

52 "API_KEY uppercase", 

53 "API_KEY = 'ABCDEFGHIJKLMNOPQRST1234567890'\n", 

54 "api", 

55 ), 

56 ( 

57 "password assignment", 

58 "password = 'super_secret_password_123'\n", 

59 "secret", 

60 ), 

61 ( 

62 "PASSWORD uppercase", 

63 "PASSWORD = 'super_secret_password_123'\n", 

64 "secret", 

65 ), 

66 ( 

67 "passwd assignment", 

68 "passwd = 'my_password_here'\n", 

69 "secret", 

70 ), 

71 ( 

72 "secret assignment", 

73 "secret = 'a_very_secret_value'\n", 

74 "secret", 

75 ), 

76 ( 

77 "token assignment", 

78 "token = 'abcdefghijklmnopqrst1234567890'\n", 

79 "secret", 

80 ), 

81 ( 

82 "AWS access key ID", 

83 "aws_access_key_id = 'AKIAIOSFODNN7EXAMPLE'\n", 

84 "AWS", 

85 ), 

86 ( 

87 "AWS secret access key", 

88 "aws_secret_access_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'\n", 

89 "AWS", 

90 ), 

91 ( 

92 "GitHub PAT", 

93 "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij\n", 

94 "ghp_", 

95 ), 

96 ( 

97 "OpenAI key", 

98 "sk-abcdefghijklmnopqrstuvwxyz\n", 

99 "sk-", 

100 ), 

101 ( 

102 "RSA private key", 

103 ( 

104 "-----BEGIN RSA PRIVATE KEY-----\n" 

105 "MIIEpAIBAAKCAQEA...\n" 

106 "-----END RSA PRIVATE KEY-----\n" 

107 ), 

108 "private key", 

109 ), 

110 ( 

111 "EC private key", 

112 ( 

113 "-----BEGIN EC PRIVATE KEY-----\n" 

114 "MIIEpAIBAAKCAQEA...\n" 

115 "-----END EC PRIVATE KEY-----\n" 

116 ), 

117 "private key", 

118 ), 

119 ( 

120 "generic private key", 

121 ( 

122 "-----BEGIN PRIVATE KEY-----\n" 

123 "MIIEpAIBAAKCAQEA...\n" 

124 "-----END PRIVATE KEY-----\n" 

125 ), 

126 "private key", 

127 ), 

128 ], 

129 ids=[ 

130 "api_key", 

131 "apikey", 

132 "api-key", 

133 "API_KEY", 

134 "password", 

135 "PASSWORD", 

136 "passwd", 

137 "secret", 

138 "token", 

139 "aws-access-key", 

140 "aws-secret-key", 

141 "github-pat", 

142 "openai-key", 

143 "rsa-private-key", 

144 "ec-private-key", 

145 "generic-private-key", 

146 ], 

147) 

148def test_detects_secret_pattern( 

149 description: str, 

150 text: str, 

151 expected_pattern: str, 

152) -> None: 

153 """Detects {description} and description mentions pattern type.""" 

154 result = scan_for_secrets(text) 

155 assert_that(result).is_not_empty() 

156 assert_that(result[0].lower()).contains(expected_pattern.lower()) 

157 

158 

159# -- scan_for_secrets: negative cases ----------------------------------------- 

160 

161 

162@pytest.mark.parametrize( 

163 ("description", "text"), 

164 [ 

165 ("short password", "password = 'short'\n"), 

166 ("short ghp_ prefix", "ghp_short\n"), 

167 ("short sk- prefix", "sk-short\n"), 

168 ], 

169 ids=["short-password", "short-ghp", "short-sk"], 

170) 

171def test_does_not_detect_short_values(description: str, text: str) -> None: 

172 """Short values ({description}) are not flagged as secrets.""" 

173 assert_that(scan_for_secrets(text)).is_empty() 

174 

175 

176# -- scan_for_secrets: multiple detections ------------------------------------- 

177 

178 

179def test_detects_multiple_secrets() -> None: 

180 """Multiple different secret types are all detected.""" 

181 text = ( 

182 "api_key = 'ABCDEFGHIJKLMNOPQRST1234567890'\n" 

183 "password = 'super_secret_password_123'\n" 

184 "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij\n" 

185 ) 

186 result = scan_for_secrets(text) 

187 assert_that(result).is_length(3) 

188 

189 

190# -- redact_secrets ------------------------------------------------------------ 

191 

192 

193def test_redact_clean_text_unchanged() -> None: 

194 """Text without secrets is returned unchanged.""" 

195 text = "def hello():\n return 'world'\n" 

196 assert_that(redact_secrets(text)).is_equal_to(text) 

197 

198 

199@pytest.mark.parametrize( 

200 ("description", "text", "forbidden_substring"), 

201 [ 

202 ( 

203 "API key", 

204 "api_key = 'ABCDEFGHIJKLMNOPQRST1234567890'\n", 

205 "ABCDEFGHIJKLMNOPQRST", 

206 ), 

207 ( 

208 "password", 

209 "password = 'super_secret_password_123'\n", 

210 "super_secret_password_123", 

211 ), 

212 ( 

213 "GitHub PAT", 

214 # nosemgrep: detected-github-token 

215 "token = ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij\n", 

216 "ghp_ABCDEFGHIJ", 

217 ), 

218 ( 

219 "OpenAI key", 

220 "key = sk-abcdefghijklmnopqrstuvwxyz\n", 

221 "sk-abcdefghij", 

222 ), 

223 ( 

224 "RSA private key", 

225 ( 

226 "-----BEGIN RSA PRIVATE KEY-----\n" 

227 "MIIEpAIBAAKCAQEA...\n" 

228 "-----END RSA PRIVATE KEY-----\n" 

229 ), 

230 "BEGIN RSA PRIVATE KEY", 

231 ), 

232 ], 

233 ids=[ 

234 "api-key", 

235 "password", 

236 "github-pat", 

237 "openai-key", 

238 "private-key", 

239 ], 

240) 

241def test_redact_replaces_secret( 

242 description: str, 

243 text: str, 

244 forbidden_substring: str, 

245) -> None: 

246 """Redaction of {description} replaces value with [REDACTED].""" 

247 result = redact_secrets(text) 

248 assert_that(result).contains("[REDACTED]") 

249 assert_that(result).does_not_contain(forbidden_substring) 

250 

251 

252def test_redact_multiple_secrets() -> None: 

253 """Multiple secrets are all redacted.""" 

254 text = ( 

255 "api_key = 'ABCDEFGHIJKLMNOPQRST1234567890'\n" 

256 "password = 'super_secret_password_123'\n" 

257 "normal_var = 42\n" 

258 ) 

259 result = redact_secrets(text) 

260 assert_that(result).contains("[REDACTED]") 

261 assert_that(result).contains("normal_var = 42") 

262 assert_that(result).does_not_contain("ABCDEFGHIJKLMNOPQRST") 

263 assert_that(result).does_not_contain("super_secret_password_123") 

264 

265 

266def test_redact_preserves_surrounding_text() -> None: 

267 """Redaction preserves non-secret text around the match.""" 

268 text = "# config\napi_key = 'ABCDEFGHIJKLMNOPQRST1234567890'\n# end\n" 

269 result = redact_secrets(text) 

270 assert_that(result).contains("# config") 

271 assert_that(result).contains("# end") 

272 assert_that(result).contains("[REDACTED]") 

273 

274 

275def test_empty_string_redact() -> None: 

276 """Empty string returns empty string.""" 

277 assert_that(redact_secrets("")).is_equal_to("")