M7 Logging-Sensitivität mit echten Log- und Persistenznachweisen
abgesichert
This commit is contained in:
@@ -753,6 +753,61 @@ class SqliteProcessingAttemptRepositoryAdapterTest {
|
|||||||
assertThat(saved.get(0).status()).isEqualTo(ProcessingStatus.PROPOSAL_READY);
|
assertThat(saved.get(0).status()).isEqualTo(ProcessingStatus.PROPOSAL_READY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// AI field persistence is independent of logging configuration
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the repository always stores the complete AI raw response and reasoning,
|
||||||
|
* independent of any logging sensitivity configuration.
|
||||||
|
* <p>
|
||||||
|
* The {@code AiContentSensitivity} setting controls only whether sensitive content is
|
||||||
|
* written to log files. It has no influence on what the repository persists. This test
|
||||||
|
* demonstrates that full AI fields are stored regardless of any logging configuration by
|
||||||
|
* verifying a round-trip with both full content and long reasoning text.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void save_persistsFullAiResponseAndReasoning_unaffectedByLoggingConfiguration() {
|
||||||
|
// The repository has no dependency on AiContentSensitivity.
|
||||||
|
// It always stores the complete AI raw response and reasoning.
|
||||||
|
DocumentFingerprint fingerprint = new DocumentFingerprint(
|
||||||
|
"d1d2d3d4d5d6d7d8d9dadbdcdddedfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfd0".substring(0, 64));
|
||||||
|
RunId runId = new RunId("persistence-independence-run");
|
||||||
|
Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
|
||||||
|
|
||||||
|
// Deliberately long and complete AI raw response — must be stored in full
|
||||||
|
String fullRawResponse = "{\"date\":\"2026-03-01\",\"title\":\"Stromabrechnung\","
|
||||||
|
+ "\"reasoning\":\"Invoice date clearly stated on page 1. Utility provider named.\"}";
|
||||||
|
// Deliberately complete reasoning — must be stored in full
|
||||||
|
String fullReasoning = "Invoice date clearly stated on page 1. Utility provider named.";
|
||||||
|
|
||||||
|
insertDocumentRecord(fingerprint);
|
||||||
|
|
||||||
|
ProcessingAttempt attempt = new ProcessingAttempt(
|
||||||
|
fingerprint, runId, 1, now, now.plusSeconds(5),
|
||||||
|
ProcessingStatus.PROPOSAL_READY,
|
||||||
|
null, null, false,
|
||||||
|
"gpt-4o", "prompt-v1.txt",
|
||||||
|
3, 750,
|
||||||
|
fullRawResponse,
|
||||||
|
fullReasoning,
|
||||||
|
LocalDate.of(2026, 3, 1), DateSource.AI_PROVIDED,
|
||||||
|
"Stromabrechnung",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
repository.save(attempt);
|
||||||
|
|
||||||
|
List<ProcessingAttempt> saved = repository.findAllByFingerprint(fingerprint);
|
||||||
|
assertThat(saved).hasSize(1);
|
||||||
|
ProcessingAttempt result = saved.get(0);
|
||||||
|
|
||||||
|
// Full raw response is stored completely — not truncated, not suppressed
|
||||||
|
assertThat(result.aiRawResponse()).isEqualTo(fullRawResponse);
|
||||||
|
// Full reasoning is stored completely — not truncated, not suppressed
|
||||||
|
assertThat(result.aiReasoning()).isEqualTo(fullReasoning);
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Integration with document records (FK constraints)
|
// Integration with document records (FK constraints)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
|||||||
|
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
|
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
|
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link Log4jProcessingLogger}.
|
* Unit tests for {@link Log4jProcessingLogger}.
|
||||||
@@ -23,6 +20,7 @@ import static org.mockito.Mockito.*;
|
|||||||
class Log4jProcessingLoggerTest {
|
class Log4jProcessingLoggerTest {
|
||||||
|
|
||||||
private Log4jProcessingLogger logger;
|
private Log4jProcessingLogger logger;
|
||||||
|
private TestLogCapture logCapture;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
@@ -30,6 +28,14 @@ class Log4jProcessingLoggerTest {
|
|||||||
logger = new Log4jProcessingLogger(Log4jProcessingLoggerTest.class);
|
logger = new Log4jProcessingLogger(Log4jProcessingLoggerTest.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDownCapture() {
|
||||||
|
if (logCapture != null) {
|
||||||
|
logCapture.stopCapture();
|
||||||
|
logCapture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void constructor_createsLoggerForClass() {
|
void constructor_createsLoggerForClass() {
|
||||||
// Verify that a logger is successfully created for the given class
|
// Verify that a logger is successfully created for the given class
|
||||||
@@ -240,202 +246,99 @@ class Log4jProcessingLoggerTest {
|
|||||||
}, "Constructor should reject null AiContentSensitivity");
|
}, "Constructor should reject null AiContentSensitivity");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Behavioral tests: Logging sensitivity enforcement =====
|
// ===== Behavioral tests: real log capture verification =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that debugSensitiveAiContent() respects PROTECT_SENSITIVE_CONTENT.
|
* Verifies that in PROTECT_SENSITIVE_CONTENT mode, the raw AI response is NOT written to any log.
|
||||||
* <p>
|
* Uses {@link TestLogCapture} to prove suppression with real log output.
|
||||||
* This test demonstrates that when PROTECT_SENSITIVE_CONTENT is active,
|
|
||||||
* the method can be called without error, and the implementation respects
|
|
||||||
* the protection setting by not emitting log output.
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void debugSensitiveAiContent_protectionMode_acceptsCall() {
|
void debugSensitiveAiContent_protectionMode_rawResponseNotLogged() {
|
||||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
||||||
Log4jProcessingLoggerTest.class,
|
Log4jProcessingLoggerTest.class,
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
||||||
|
String sensitiveContent = "PROTECT_RAW_RESPONSE_9f3e2b1a";
|
||||||
|
|
||||||
// Act: Call with sensitive content in protection mode
|
logCapture = new TestLogCapture();
|
||||||
String sensitiveContent = "Complete AI response: {\"title\": \"Stromrechnung\"}";
|
logCapture.startCapture();
|
||||||
assertDoesNotThrow(() -> {
|
|
||||||
protectedLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
|
||||||
}, "Protection mode should accept debugSensitiveAiContent() calls without error");
|
|
||||||
|
|
||||||
// The actual protection check happens inside Log4jProcessingLogger.debugSensitiveAiContent():
|
|
||||||
// it checks: if (aiContentSensitivity == AiContentSensitivity.LOG_SENSITIVE_CONTENT)
|
|
||||||
// In protection mode, the log4jLogger.debug() is NOT called.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that debugSensitiveAiContent() respects LOG_SENSITIVE_CONTENT.
|
|
||||||
* <p>
|
|
||||||
* This test demonstrates that when LOG_SENSITIVE_CONTENT is explicitly enabled,
|
|
||||||
* the debugSensitiveAiContent() method works identically and the implementation
|
|
||||||
* logs the content by delegating to Log4j2 at DEBUG level.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void debugSensitiveAiContent_explicitOptIn_acceptsCall() {
|
|
||||||
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
|
||||||
|
|
||||||
// Act: Call with sensitive content in opt-in mode
|
|
||||||
String sensitiveContent = "Complete AI response: {\"title\": \"Stromrechnung\"}";
|
|
||||||
assertDoesNotThrow(() -> {
|
|
||||||
optInLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
|
||||||
}, "Opt-in mode should accept debugSensitiveAiContent() calls without error");
|
|
||||||
|
|
||||||
// The actual logging happens inside Log4jProcessingLogger.debugSensitiveAiContent():
|
|
||||||
// it checks: if (aiContentSensitivity == AiContentSensitivity.LOG_SENSITIVE_CONTENT)
|
|
||||||
// In opt-in mode, log4jLogger.debug(message, args) IS called.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the reasoning field is also protected by the sensitivity rule.
|
|
||||||
* <p>
|
|
||||||
* This test ensures that not only the raw AI response but also the reasoning
|
|
||||||
* field is subject to the same protection/opt-in behavior.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void debugSensitiveAiContent_reasoning_respectsSensitivity() {
|
|
||||||
// Test both modes
|
|
||||||
AiContentSensitivity[] modes = {
|
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT,
|
|
||||||
AiContentSensitivity.LOG_SENSITIVE_CONTENT
|
|
||||||
};
|
|
||||||
|
|
||||||
for (AiContentSensitivity mode : modes) {
|
|
||||||
// Create logger in the current mode
|
|
||||||
Log4jProcessingLogger logger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
mode);
|
|
||||||
|
|
||||||
// Verify that the method exists and is callable
|
|
||||||
// The actual sensitivity decision is made inside debugSensitiveAiContent()
|
|
||||||
assertDoesNotThrow(() -> {
|
|
||||||
logger.debugSensitiveAiContent("AI reasoning: {}", "reasoning text");
|
|
||||||
}, "Both sensitivity modes should accept reasoning messages");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that non-sensitive info/debug methods are NOT controlled by AiContentSensitivity.
|
|
||||||
* <p>
|
|
||||||
* This test ensures that the sensitivity rule applies ONLY to debugSensitiveAiContent(),
|
|
||||||
* not to regular debug() or info() methods.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void regularDebugMethod_notAffectedBySensitivitySetting() {
|
|
||||||
// Create loggers in both modes
|
|
||||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
|
||||||
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
|
||||||
|
|
||||||
// Both should accept regular debug() calls
|
|
||||||
assertDoesNotThrow(() -> {
|
|
||||||
protectedLogger.debug("Regular message in protection mode");
|
|
||||||
optInLogger.debug("Regular message in opt-in mode");
|
|
||||||
}, "Regular debug() should work regardless of sensitivity setting");
|
|
||||||
|
|
||||||
// Both should accept regular info() calls
|
|
||||||
assertDoesNotThrow(() -> {
|
|
||||||
protectedLogger.info("Info in protection mode");
|
|
||||||
optInLogger.info("Info in opt-in mode");
|
|
||||||
}, "Regular info() should work regardless of sensitivity setting");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that SQLite persistence is independent of logging sensitivity.
|
|
||||||
* <p>
|
|
||||||
* This test documents that the logging sensitivity rule does NOT affect
|
|
||||||
* whether sensitive content is persisted. Sensitive content remains in SQLite
|
|
||||||
* regardless of the log.ai.sensitive setting.
|
|
||||||
* <p>
|
|
||||||
* Note: This is a documentation test that verifies the interface contract.
|
|
||||||
* The actual SQLite persistence is verified in integration tests.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void sensitivityRule_doesNotAffectSqlitePersistence() {
|
|
||||||
// Verify that both protection and opt-in modes exist
|
|
||||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
|
||||||
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
|
||||||
|
|
||||||
// The interface contract states that both loggers must be creatable
|
|
||||||
assertNotNull(protectedLogger, "Protect mode logger must be creatable");
|
|
||||||
assertNotNull(optInLogger, "Opt-in mode logger must be creatable");
|
|
||||||
|
|
||||||
// Both have the debugSensitiveAiContent method available
|
|
||||||
// The actual persistence is handled independently in the repository layer
|
|
||||||
// This test verifies that the logger API is consistent regardless of sensitivity
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Behavioral tests with Mockito spy verification =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that in protection mode, debugSensitiveAiContent() does NOT call the underlying logger.
|
|
||||||
* <p>
|
|
||||||
* This test uses Mockito to spy on the Log4j2 logger and verify that the debug() method
|
|
||||||
* is never called in protection mode.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void debugSensitiveAiContent_protectionMode_doesNotCallLog4jDebug() {
|
|
||||||
// Setup: Create logger with spied Log4j2 logger
|
|
||||||
Logger spiedLog4jLogger = spy(LogManager.getLogger(Log4jProcessingLoggerTest.class));
|
|
||||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT) {
|
|
||||||
// Override to use spied logger
|
|
||||||
private final Logger log4jLoggerInstance = spiedLog4jLogger;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act: Call debugSensitiveAiContent with sensitive content
|
|
||||||
String sensitiveContent = "{\"title\": \"Stromrechnung\"}";
|
|
||||||
protectedLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
protectedLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
||||||
|
logCapture.stopCapture();
|
||||||
|
|
||||||
// Assert: The underlying Log4j debug() should never be called in protection mode
|
assertFalse(logCapture.containsMessage(sensitiveContent),
|
||||||
// This verifies the BEHAVIOR: protection mode suppresses the log call
|
"PROTECT_SENSITIVE_CONTENT mode must not write raw AI response to log");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that in opt-in mode, debugSensitiveAiContent() calls the underlying logger.
|
* Verifies that in LOG_SENSITIVE_CONTENT mode, the raw AI response IS written to log.
|
||||||
* <p>
|
* Uses {@link TestLogCapture} to prove emission with real log output.
|
||||||
* This test uses logic to verify that LOG_SENSITIVE_CONTENT mode allows the debug call.
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void debugSensitiveAiContent_optInMode_implementationCallsDebug() {
|
void debugSensitiveAiContent_optInMode_rawResponseLogged() {
|
||||||
// Setup: Create logger in opt-in mode
|
|
||||||
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
|
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
|
||||||
Log4jProcessingLoggerTest.class,
|
Log4jProcessingLoggerTest.class,
|
||||||
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
||||||
|
String sensitiveContent = "OPTIN_RAW_RESPONSE_7d4c8f2e";
|
||||||
|
|
||||||
// Act: Call debugSensitiveAiContent
|
logCapture = new TestLogCapture();
|
||||||
String sensitiveContent = "{\"title\": \"Stromrechnung\"}";
|
logCapture.startCapture();
|
||||||
// This should not throw - it should call log4jLogger.debug()
|
optInLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
||||||
assertDoesNotThrow(() -> {
|
logCapture.stopCapture();
|
||||||
optInLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
|
||||||
}, "Opt-in mode should allow debug() calls without error");
|
|
||||||
|
|
||||||
// Assert: The fact that it didn't throw is implicit verification that
|
assertTrue(logCapture.containsMessage(sensitiveContent),
|
||||||
// LOG_SENSITIVE_CONTENT mode calls the underlying logger correctly
|
"LOG_SENSITIVE_CONTENT mode must write raw AI response to log when explicitly enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies the critical behavioral contrast: protection mode vs. opt-in mode.
|
* Verifies that in PROTECT_SENSITIVE_CONTENT mode, the AI reasoning field is NOT written to log.
|
||||||
* <p>
|
* Uses {@link TestLogCapture} to prove suppression of the reasoning field specifically.
|
||||||
* This test demonstrates that debugSensitiveAiContent() behaves differently
|
|
||||||
* depending on the AiContentSensitivity setting. With PROTECT, it does nothing.
|
|
||||||
* With LOG, it delegates to the underlying logger.
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void debugSensitiveAiContent_modeContrast_behaviorDiffers() {
|
void debugSensitiveAiContent_protectionMode_reasoningNotLogged() {
|
||||||
// Setup
|
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
||||||
|
Log4jProcessingLoggerTest.class,
|
||||||
|
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
||||||
|
String sensitiveReasoning = "PROTECT_REASONING_c8a1e5f3";
|
||||||
|
|
||||||
|
logCapture = new TestLogCapture();
|
||||||
|
logCapture.startCapture();
|
||||||
|
protectedLogger.debugSensitiveAiContent("AI reasoning: {}", sensitiveReasoning);
|
||||||
|
logCapture.stopCapture();
|
||||||
|
|
||||||
|
assertFalse(logCapture.containsMessage(sensitiveReasoning),
|
||||||
|
"PROTECT_SENSITIVE_CONTENT mode must not write AI reasoning to log");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that in LOG_SENSITIVE_CONTENT mode, the AI reasoning field IS written to log.
|
||||||
|
* Uses {@link TestLogCapture} to prove emission of the reasoning field specifically.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void debugSensitiveAiContent_optInMode_reasoningLogged() {
|
||||||
|
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
|
||||||
|
Log4jProcessingLoggerTest.class,
|
||||||
|
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
||||||
|
String sensitiveReasoning = "OPTIN_REASONING_b2d6f9a4";
|
||||||
|
|
||||||
|
logCapture = new TestLogCapture();
|
||||||
|
logCapture.startCapture();
|
||||||
|
optInLogger.debugSensitiveAiContent("AI reasoning: {}", sensitiveReasoning);
|
||||||
|
logCapture.stopCapture();
|
||||||
|
|
||||||
|
assertTrue(logCapture.containsMessage(sensitiveReasoning),
|
||||||
|
"LOG_SENSITIVE_CONTENT mode must write AI reasoning to log when explicitly enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the behavioral contrast between protection and opt-in mode using real log capture.
|
||||||
|
* <p>
|
||||||
|
* The same sensitive content token is logged in both modes: protection mode must suppress it,
|
||||||
|
* opt-in mode must emit it. Proves the conditional in {@code debugSensitiveAiContent} works
|
||||||
|
* in both directions.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void debugSensitiveAiContent_modeContrast_differentLogOutput() {
|
||||||
|
String sensitiveContent = "CONTRAST_SENSITIVE_d3e7a1b8";
|
||||||
|
|
||||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
||||||
Log4jProcessingLoggerTest.class,
|
Log4jProcessingLoggerTest.class,
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
||||||
@@ -443,87 +346,44 @@ class Log4jProcessingLoggerTest {
|
|||||||
Log4jProcessingLoggerTest.class,
|
Log4jProcessingLoggerTest.class,
|
||||||
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
||||||
|
|
||||||
String sensitiveContent = "{\"title\": \"Test\"}";
|
// Protection mode: content must NOT appear in logs
|
||||||
|
logCapture = new TestLogCapture();
|
||||||
|
logCapture.startCapture();
|
||||||
|
protectedLogger.debugSensitiveAiContent("AI: {}", sensitiveContent);
|
||||||
|
logCapture.stopCapture();
|
||||||
|
assertFalse(logCapture.containsMessage(sensitiveContent),
|
||||||
|
"PROTECT mode must suppress sensitive content");
|
||||||
|
|
||||||
// Act & Assert: Both should execute without error, but the BEHAVIOR is different
|
// Opt-in mode: same content MUST appear in logs
|
||||||
// In protection mode: log4jLogger.debug() is NOT called
|
logCapture = new TestLogCapture();
|
||||||
// In opt-in mode: log4jLogger.debug() IS called
|
logCapture.startCapture();
|
||||||
assertDoesNotThrow(() -> protectedLogger.debugSensitiveAiContent("AI: {}", sensitiveContent));
|
optInLogger.debugSensitiveAiContent("AI: {}", sensitiveContent);
|
||||||
assertDoesNotThrow(() -> optInLogger.debugSensitiveAiContent("AI: {}", sensitiveContent));
|
logCapture.stopCapture();
|
||||||
|
assertTrue(logCapture.containsMessage(sensitiveContent),
|
||||||
// The behavioral difference is encoded in the conditional inside debugSensitiveAiContent():
|
"LOG mode must emit sensitive content");
|
||||||
// if (aiContentSensitivity == AiContentSensitivity.LOG_SENSITIVE_CONTENT) {
|
|
||||||
// log4jLogger.debug(message, args);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that regular logging methods (debug, info) are NOT affected by sensitivity setting.
|
* Verifies that regular {@code debug()} calls always produce log output,
|
||||||
|
* even when the logger is configured with PROTECT_SENSITIVE_CONTENT.
|
||||||
* <p>
|
* <p>
|
||||||
* This test ensures that only debugSensitiveAiContent() respects the sensitivity setting.
|
* The sensitivity rule applies only to {@code debugSensitiveAiContent()},
|
||||||
|
* not to the regular logging methods.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void regularLoggingMethods_notAffectedBySensitivitySetting() {
|
void regularDebugMethod_alwaysLogged_unaffectedBySensitivity() {
|
||||||
// Create logger in protection mode
|
|
||||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
||||||
Log4jProcessingLoggerTest.class,
|
Log4jProcessingLoggerTest.class,
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
||||||
|
String regularMarker = "REGULAR_DEBUG_e6f0c2d9";
|
||||||
|
|
||||||
// Act: Call regular logging methods
|
logCapture = new TestLogCapture();
|
||||||
String message = "Regular message";
|
logCapture.startCapture();
|
||||||
assertDoesNotThrow(() -> {
|
protectedLogger.debug("Regular: {}", regularMarker);
|
||||||
protectedLogger.info(message);
|
logCapture.stopCapture();
|
||||||
protectedLogger.debug("Debug: {}", message);
|
|
||||||
}, "Regular logging methods should work regardless of sensitivity setting");
|
|
||||||
|
|
||||||
// Assert: The fact that both succeeded shows they're not affected by PROTECT_SENSITIVE_CONTENT
|
assertTrue(logCapture.containsMessage(regularMarker),
|
||||||
}
|
"Regular debug() must always produce log output regardless of sensitivity setting");
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that debugSensitiveAiContent respects its setting on every call.
|
|
||||||
* <p>
|
|
||||||
* This test confirms that the sensitivity decision is consistently applied.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void debugSensitiveAiContent_sensitivityDecisionPerCall_consistent() {
|
|
||||||
// Setup: Create a protection-mode logger
|
|
||||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
|
||||||
|
|
||||||
// Act: Make multiple sensitive log calls
|
|
||||||
assertDoesNotThrow(() -> {
|
|
||||||
protectedLogger.debugSensitiveAiContent("First: {}", "sensitive-1");
|
|
||||||
protectedLogger.debugSensitiveAiContent("Second: {}", "sensitive-2");
|
|
||||||
protectedLogger.debugSensitiveAiContent("Third: {}", "sensitive-3");
|
|
||||||
}, "All debugSensitiveAiContent calls should execute without error");
|
|
||||||
|
|
||||||
// Assert: The setting is consistently applied across all calls
|
|
||||||
// (In protection mode, all suppress the actual log output)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that sensitivity setting does NOT affect SQLite persistence independently.
|
|
||||||
* <p>
|
|
||||||
* This test confirms that the logger only controls log file output, not persistence.
|
|
||||||
* The logger has no knowledge of or control over SQLite persistence.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void sensitivityRule_doesNotAffectPersistenceDecisions() {
|
|
||||||
// Both modes should have the same method signature and behavior
|
|
||||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
|
||||||
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
|
|
||||||
Log4jProcessingLoggerTest.class,
|
|
||||||
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
|
||||||
|
|
||||||
// Both have the same interface and should support the same calls
|
|
||||||
assertDoesNotThrow(() -> protectedLogger.debugSensitiveAiContent("AI: {}", "data"));
|
|
||||||
assertDoesNotThrow(() -> optInLogger.debugSensitiveAiContent("AI: {}", "data"));
|
|
||||||
|
|
||||||
// The logger itself doesn't control persistence - that's in the repository layer.
|
|
||||||
// This test verifies the logger interface is consistent regardless of sensitivity.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.Level;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.core.LogEvent;
|
||||||
import org.apache.logging.log4j.core.LoggerContext;
|
import org.apache.logging.log4j.core.LoggerContext;
|
||||||
import org.apache.logging.log4j.core.appender.AbstractAppender;
|
import org.apache.logging.log4j.core.appender.AbstractAppender;
|
||||||
|
import org.apache.logging.log4j.core.config.Configuration;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test utility that captures Log4j2 log events during test execution.
|
* Test utility that captures Log4j2 log events during test execution.
|
||||||
* <p>
|
* <p>
|
||||||
* This class temporarily attaches a ListAppender to the root logger
|
* Registers a list appender on the root {@link org.apache.logging.log4j.core.config.LoggerConfig}
|
||||||
* to capture all log messages, allowing behavioral verification of logging
|
* via the Log4j2 {@link Configuration} API so that events from all descendant loggers propagate
|
||||||
* decisions in unit tests.
|
* to the appender correctly. Call {@link #startCapture()} before the code under test and
|
||||||
|
* {@link #stopCapture()} after it to bracket the capture window.
|
||||||
* <p>
|
* <p>
|
||||||
* Usage:
|
* Usage:
|
||||||
* <pre>
|
* <pre>
|
||||||
@@ -30,34 +35,41 @@ import java.util.Objects;
|
|||||||
class TestLogCapture {
|
class TestLogCapture {
|
||||||
|
|
||||||
private final ListAppender listAppender;
|
private final ListAppender listAppender;
|
||||||
private org.apache.logging.log4j.core.Logger rootLogger;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new TestLogCapture that will capture all log events.
|
* Creates a new TestLogCapture with a uniquely named internal appender.
|
||||||
*/
|
*/
|
||||||
TestLogCapture() {
|
TestLogCapture() {
|
||||||
this.listAppender = new ListAppender();
|
// Unique name avoids conflicts when multiple instances are created in one test run
|
||||||
|
this.listAppender = new ListAppender("TestListAppender-" + UUID.randomUUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts capturing log events from the root logger.
|
* Starts capturing log events by registering the appender on the root logger configuration.
|
||||||
* <p>
|
* <p>
|
||||||
* Attaches a ListAppender to the root logger to capture all messages.
|
* Uses {@link Configuration#getRootLogger()} and {@link LoggerContext#updateLoggers()} so
|
||||||
|
* that events from all descendant loggers propagate through the configuration hierarchy to
|
||||||
|
* the appender.
|
||||||
*/
|
*/
|
||||||
void startCapture() {
|
void startCapture() {
|
||||||
LoggerContext context = (LoggerContext) LogManager.getContext(false);
|
LoggerContext context = (LoggerContext) LogManager.getContext(false);
|
||||||
this.rootLogger = context.getRootLogger();
|
Configuration config = context.getConfiguration();
|
||||||
this.listAppender.start();
|
listAppender.start();
|
||||||
this.rootLogger.addAppender(this.listAppender);
|
config.addAppender(listAppender);
|
||||||
|
config.getRootLogger().addAppender(listAppender, Level.ALL, null);
|
||||||
|
context.updateLoggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops capturing log events and removes the appender.
|
* Stops capturing and removes the appender from the root logger configuration.
|
||||||
*/
|
*/
|
||||||
void stopCapture() {
|
void stopCapture() {
|
||||||
if (this.rootLogger != null) {
|
LoggerContext context = (LoggerContext) LogManager.getContext(false);
|
||||||
this.rootLogger.removeAppender(this.listAppender);
|
Configuration config = context.getConfiguration();
|
||||||
this.listAppender.stop();
|
config.getRootLogger().removeAppender(listAppender.getName());
|
||||||
|
context.updateLoggers();
|
||||||
|
if (listAppender.isStarted()) {
|
||||||
|
listAppender.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +81,7 @@ class TestLogCapture {
|
|||||||
*/
|
*/
|
||||||
boolean containsMessage(String substring) {
|
boolean containsMessage(String substring) {
|
||||||
Objects.requireNonNull(substring, "substring must not be null");
|
Objects.requireNonNull(substring, "substring must not be null");
|
||||||
return this.listAppender.getEvents()
|
return listAppender.getEvents()
|
||||||
.stream()
|
.stream()
|
||||||
.anyMatch(event -> event.getMessage().getFormattedMessage().contains(substring));
|
.anyMatch(event -> event.getMessage().getFormattedMessage().contains(substring));
|
||||||
}
|
}
|
||||||
@@ -80,7 +92,7 @@ class TestLogCapture {
|
|||||||
* @return list of formatted log messages
|
* @return list of formatted log messages
|
||||||
*/
|
*/
|
||||||
List<String> getMessages() {
|
List<String> getMessages() {
|
||||||
return this.listAppender.getEvents()
|
return listAppender.getEvents()
|
||||||
.stream()
|
.stream()
|
||||||
.map(event -> event.getMessage().getFormattedMessage())
|
.map(event -> event.getMessage().getFormattedMessage())
|
||||||
.toList();
|
.toList();
|
||||||
@@ -90,25 +102,24 @@ class TestLogCapture {
|
|||||||
* Clears all captured log events.
|
* Clears all captured log events.
|
||||||
*/
|
*/
|
||||||
void clear() {
|
void clear() {
|
||||||
this.listAppender.getEvents().clear();
|
listAppender.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal ListAppender for collecting log events.
|
* Internal appender that collects immutable copies of log events.
|
||||||
*/
|
*/
|
||||||
private static class ListAppender extends AbstractAppender {
|
private static class ListAppender extends AbstractAppender {
|
||||||
|
|
||||||
private final List<org.apache.logging.log4j.core.LogEvent> events = new ArrayList<>();
|
private final List<LogEvent> events = new ArrayList<>();
|
||||||
|
|
||||||
ListAppender() {
|
ListAppender(String name) {
|
||||||
super("TestListAppender", null, null, false);
|
super(name, null, null, false);
|
||||||
// Set filter and layout to ensure events are processed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void append(org.apache.logging.log4j.core.LogEvent event) {
|
public void append(LogEvent event) {
|
||||||
// Copy event to ensure it's captured even if the original is reused
|
// Store an immutable copy to prevent issues with Log4j2 event object reuse
|
||||||
this.events.add(event);
|
events.add(event.toImmutable());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -116,8 +127,12 @@ class TestLogCapture {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<org.apache.logging.log4j.core.LogEvent> getEvents() {
|
List<LogEvent> getEvents() {
|
||||||
return new ArrayList<>(this.events);
|
return new ArrayList<>(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
events.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user