from __future__ import annotations import unittest from ai_triage.redaction import REDACTED, redact_data, redact_text class RedactTextTests(unittest.TestCase): def test_redacts_headers_json_credentials_email_dsn_and_url_secrets(self) -> None: source = "\n".join( ( "Authorization: Bearer bearer-secret", "Cookie: session=cookie-secret", "Set-Cookie: sid=set-cookie-secret; Path=/; HttpOnly", "X-API-Key: header-secret", '{"apiKey":"json-key","password":"json-password","accountEmail":"qa@example.com"}', "dsn=mysql://root:dsn-password@db.example.com:4000/app?tls=true&token=dsn-query-secret", "GET https://example.com/v1/items?page=1&token=query-token&sig=sas-signature", "saved_placeholder:map[open_api_key:public:private-secret account_pwd:placeholder-password]", ) ) result = redact_text(source) for secret in ( "bearer-secret", "cookie-secret", "set-cookie-secret", "header-secret", "json-key", "json-password", "qa@example.com", "dsn-password", "dsn-query-secret", "query-token", "sas-signature", "private-secret", "placeholder-password", ): self.assertNotIn(secret, result) self.assertIn("page=1", result) self.assertIn(REDACTED, result) def test_truncates_after_redaction_to_the_requested_maximum(self) -> None: result = redact_text("token=do-not-leak " + ("x" * 200), max_length=48) self.assertLessEqual(len(result), 48) self.assertNotIn("do-not-leak", result) self.assertTrue(result.endswith("[TRUNCATED]")) def test_url_redaction_remains_encoded_and_preserves_safe_query_values(self) -> None: result = redact_text("https://example.com/items?api_key=secret&page=1") self.assertEqual(result, "https://example.com/items?api_key=%5BREDACTED%5D&page=1") def test_redacts_prefixed_dsn_and_camel_case_cloud_credentials(self) -> None: source = "\n".join( ( "OPEN_API_DEDICATED_DB_API_TESTER_DSN=user:SYNTHETIC_PASSWORD@tcp(db.invalid:4000)/db", "dsn=user:SYNTHETIC_DSN_PASSWORD@tcp(db.invalid:4000)/db", "azureBlobSasToken=SYNTHETIC_SAS", "accessKeyId=SYNTHETIC_ACCESS_KEY", "iamAwsKeySecret=SYNTHETIC_KEY_SECRET", ) ) result = redact_text(source) for secret in ( "SYNTHETIC_PASSWORD", "SYNTHETIC_DSN_PASSWORD", "SYNTHETIC_SAS", "SYNTHETIC_ACCESS_KEY", "SYNTHETIC_KEY_SECRET", ): self.assertNotIn(secret, result) class RedactDataTests(unittest.TestCase): def test_recursively_redacts_sensitive_keys_and_string_values(self) -> None: source = { "authorization": "Basic basic-secret", "nested": { "password": "nested-secret", "url": "https://example.com/callback?code=oauth-code&safe=yes", }, "items": ["owner@example.com", {"api_token": "item-secret"}], } result = redact_data(source) self.assertEqual(result["authorization"], REDACTED) self.assertEqual(result["nested"]["password"], REDACTED) self.assertNotIn("oauth-code", result["nested"]["url"]) self.assertIn("safe=yes", result["nested"]["url"]) self.assertEqual(result["items"][0], REDACTED) self.assertEqual(result["items"][1]["api_token"], REDACTED) def test_redacts_camel_case_and_prefixed_sensitive_mapping_keys(self) -> None: result = redact_data( { "azureBlobSasToken": "synthetic-sas", "accessKeyId": "synthetic-access", "OPEN_API_DEDICATED_DB_API_TESTER_DSN": "synthetic-dsn", } ) self.assertEqual(result["azureBlobSasToken"], REDACTED) self.assertEqual(result["accessKeyId"], REDACTED) self.assertEqual(result["OPEN_API_DEDICATED_DB_API_TESTER_DSN"], REDACTED) if __name__ == "__main__": unittest.main()