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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for AI secrets scanning and redaction."""
3from __future__ import annotations
5import pytest
6from assertpy import assert_that
8from lintro.ai.secrets import redact_secrets, scan_for_secrets
10# -- scan_for_secrets: no secrets ---------------------------------------------
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()
19def test_empty_string_no_secrets() -> None:
20 """Empty string returns no secrets."""
21 assert_that(scan_for_secrets("")).is_empty()
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()
30# -- scan_for_secrets: pattern detection (parametrized) -----------------------
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())
159# -- scan_for_secrets: negative cases -----------------------------------------
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()
176# -- scan_for_secrets: multiple detections -------------------------------------
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)
190# -- redact_secrets ------------------------------------------------------------
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)
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)
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")
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]")
275def test_empty_string_redact() -> None:
276 """Empty string returns empty string."""
277 assert_that(redact_secrets("")).is_equal_to("")