M7 Logging-Sensitivität mit echten Log- und Persistenznachweisen
abgesichert
This commit is contained in:
@@ -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.ProcessingLogger;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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 static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Log4jProcessingLogger}.
|
||||
@@ -23,6 +20,7 @@ import static org.mockito.Mockito.*;
|
||||
class Log4jProcessingLoggerTest {
|
||||
|
||||
private Log4jProcessingLogger logger;
|
||||
private TestLogCapture logCapture;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@@ -30,6 +28,14 @@ class Log4jProcessingLoggerTest {
|
||||
logger = new Log4jProcessingLogger(Log4jProcessingLoggerTest.class);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDownCapture() {
|
||||
if (logCapture != null) {
|
||||
logCapture.stopCapture();
|
||||
logCapture = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_createsLoggerForClass() {
|
||||
// Verify that a logger is successfully created for the given class
|
||||
@@ -240,202 +246,99 @@ class Log4jProcessingLoggerTest {
|
||||
}, "Constructor should reject null AiContentSensitivity");
|
||||
}
|
||||
|
||||
// ===== Behavioral tests: Logging sensitivity enforcement =====
|
||||
// ===== Behavioral tests: real log capture verification =====
|
||||
|
||||
/**
|
||||
* Verifies that debugSensitiveAiContent() respects PROTECT_SENSITIVE_CONTENT.
|
||||
* <p>
|
||||
* 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.
|
||||
* Verifies that in PROTECT_SENSITIVE_CONTENT mode, the raw AI response is NOT written to any log.
|
||||
* Uses {@link TestLogCapture} to prove suppression with real log output.
|
||||
*/
|
||||
@Test
|
||||
void debugSensitiveAiContent_protectionMode_acceptsCall() {
|
||||
void debugSensitiveAiContent_protectionMode_rawResponseNotLogged() {
|
||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
||||
Log4jProcessingLoggerTest.class,
|
||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
||||
String sensitiveContent = "PROTECT_RAW_RESPONSE_9f3e2b1a";
|
||||
|
||||
// Act: Call with sensitive content in protection mode
|
||||
String sensitiveContent = "Complete AI response: {\"title\": \"Stromrechnung\"}";
|
||||
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\"}";
|
||||
logCapture = new TestLogCapture();
|
||||
logCapture.startCapture();
|
||||
protectedLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
||||
logCapture.stopCapture();
|
||||
|
||||
// Assert: The underlying Log4j debug() should never be called in protection mode
|
||||
// This verifies the BEHAVIOR: protection mode suppresses the log call
|
||||
assertFalse(logCapture.containsMessage(sensitiveContent),
|
||||
"PROTECT_SENSITIVE_CONTENT mode must not write raw AI response to log");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that in opt-in mode, debugSensitiveAiContent() calls the underlying logger.
|
||||
* <p>
|
||||
* This test uses logic to verify that LOG_SENSITIVE_CONTENT mode allows the debug call.
|
||||
* Verifies that in LOG_SENSITIVE_CONTENT mode, the raw AI response IS written to log.
|
||||
* Uses {@link TestLogCapture} to prove emission with real log output.
|
||||
*/
|
||||
@Test
|
||||
void debugSensitiveAiContent_optInMode_implementationCallsDebug() {
|
||||
// Setup: Create logger in opt-in mode
|
||||
void debugSensitiveAiContent_optInMode_rawResponseLogged() {
|
||||
Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
|
||||
Log4jProcessingLoggerTest.class,
|
||||
AiContentSensitivity.LOG_SENSITIVE_CONTENT);
|
||||
String sensitiveContent = "OPTIN_RAW_RESPONSE_7d4c8f2e";
|
||||
|
||||
// Act: Call debugSensitiveAiContent
|
||||
String sensitiveContent = "{\"title\": \"Stromrechnung\"}";
|
||||
// This should not throw - it should call log4jLogger.debug()
|
||||
assertDoesNotThrow(() -> {
|
||||
optInLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
||||
}, "Opt-in mode should allow debug() calls without error");
|
||||
logCapture = new TestLogCapture();
|
||||
logCapture.startCapture();
|
||||
optInLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
|
||||
logCapture.stopCapture();
|
||||
|
||||
// Assert: The fact that it didn't throw is implicit verification that
|
||||
// LOG_SENSITIVE_CONTENT mode calls the underlying logger correctly
|
||||
assertTrue(logCapture.containsMessage(sensitiveContent),
|
||||
"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.
|
||||
* <p>
|
||||
* 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.
|
||||
* Verifies that in PROTECT_SENSITIVE_CONTENT mode, the AI reasoning field is NOT written to log.
|
||||
* Uses {@link TestLogCapture} to prove suppression of the reasoning field specifically.
|
||||
*/
|
||||
@Test
|
||||
void debugSensitiveAiContent_modeContrast_behaviorDiffers() {
|
||||
// Setup
|
||||
void debugSensitiveAiContent_protectionMode_reasoningNotLogged() {
|
||||
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(
|
||||
Log4jProcessingLoggerTest.class,
|
||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
||||
@@ -443,87 +346,44 @@ class Log4jProcessingLoggerTest {
|
||||
Log4jProcessingLoggerTest.class,
|
||||
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
|
||||
// In protection mode: log4jLogger.debug() is NOT called
|
||||
// In opt-in mode: log4jLogger.debug() IS called
|
||||
assertDoesNotThrow(() -> protectedLogger.debugSensitiveAiContent("AI: {}", sensitiveContent));
|
||||
assertDoesNotThrow(() -> optInLogger.debugSensitiveAiContent("AI: {}", sensitiveContent));
|
||||
|
||||
// The behavioral difference is encoded in the conditional inside debugSensitiveAiContent():
|
||||
// if (aiContentSensitivity == AiContentSensitivity.LOG_SENSITIVE_CONTENT) {
|
||||
// log4jLogger.debug(message, args);
|
||||
// }
|
||||
// Opt-in mode: same content MUST appear in logs
|
||||
logCapture = new TestLogCapture();
|
||||
logCapture.startCapture();
|
||||
optInLogger.debugSensitiveAiContent("AI: {}", sensitiveContent);
|
||||
logCapture.stopCapture();
|
||||
assertTrue(logCapture.containsMessage(sensitiveContent),
|
||||
"LOG mode must emit sensitive content");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
* 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
|
||||
void regularLoggingMethods_notAffectedBySensitivitySetting() {
|
||||
// Create logger in protection mode
|
||||
void regularDebugMethod_alwaysLogged_unaffectedBySensitivity() {
|
||||
Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
|
||||
Log4jProcessingLoggerTest.class,
|
||||
AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
|
||||
String regularMarker = "REGULAR_DEBUG_e6f0c2d9";
|
||||
|
||||
// Act: Call regular logging methods
|
||||
String message = "Regular message";
|
||||
assertDoesNotThrow(() -> {
|
||||
protectedLogger.info(message);
|
||||
protectedLogger.debug("Debug: {}", message);
|
||||
}, "Regular logging methods should work regardless of sensitivity setting");
|
||||
logCapture = new TestLogCapture();
|
||||
logCapture.startCapture();
|
||||
protectedLogger.debug("Regular: {}", regularMarker);
|
||||
logCapture.stopCapture();
|
||||
|
||||
// Assert: The fact that both succeeded shows they're not affected by PROTECT_SENSITIVE_CONTENT
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
assertTrue(logCapture.containsMessage(regularMarker),
|
||||
"Regular debug() must always produce log output regardless of sensitivity setting");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap.adapter;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
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.appender.AbstractAppender;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Test utility that captures Log4j2 log events during test execution.
|
||||
* <p>
|
||||
* This class temporarily attaches a ListAppender to the root logger
|
||||
* to capture all log messages, allowing behavioral verification of logging
|
||||
* decisions in unit tests.
|
||||
* Registers a list appender on the root {@link org.apache.logging.log4j.core.config.LoggerConfig}
|
||||
* via the Log4j2 {@link Configuration} API so that events from all descendant loggers propagate
|
||||
* to the appender correctly. Call {@link #startCapture()} before the code under test and
|
||||
* {@link #stopCapture()} after it to bracket the capture window.
|
||||
* <p>
|
||||
* Usage:
|
||||
* <pre>
|
||||
@@ -30,34 +35,41 @@ import java.util.Objects;
|
||||
class TestLogCapture {
|
||||
|
||||
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() {
|
||||
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>
|
||||
* 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() {
|
||||
LoggerContext context = (LoggerContext) LogManager.getContext(false);
|
||||
this.rootLogger = context.getRootLogger();
|
||||
this.listAppender.start();
|
||||
this.rootLogger.addAppender(this.listAppender);
|
||||
Configuration config = context.getConfiguration();
|
||||
listAppender.start();
|
||||
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() {
|
||||
if (this.rootLogger != null) {
|
||||
this.rootLogger.removeAppender(this.listAppender);
|
||||
this.listAppender.stop();
|
||||
LoggerContext context = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = context.getConfiguration();
|
||||
config.getRootLogger().removeAppender(listAppender.getName());
|
||||
context.updateLoggers();
|
||||
if (listAppender.isStarted()) {
|
||||
listAppender.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +81,7 @@ class TestLogCapture {
|
||||
*/
|
||||
boolean containsMessage(String substring) {
|
||||
Objects.requireNonNull(substring, "substring must not be null");
|
||||
return this.listAppender.getEvents()
|
||||
return listAppender.getEvents()
|
||||
.stream()
|
||||
.anyMatch(event -> event.getMessage().getFormattedMessage().contains(substring));
|
||||
}
|
||||
@@ -80,7 +92,7 @@ class TestLogCapture {
|
||||
* @return list of formatted log messages
|
||||
*/
|
||||
List<String> getMessages() {
|
||||
return this.listAppender.getEvents()
|
||||
return listAppender.getEvents()
|
||||
.stream()
|
||||
.map(event -> event.getMessage().getFormattedMessage())
|
||||
.toList();
|
||||
@@ -90,25 +102,24 @@ class TestLogCapture {
|
||||
* Clears all captured log events.
|
||||
*/
|
||||
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 final List<org.apache.logging.log4j.core.LogEvent> events = new ArrayList<>();
|
||||
private final List<LogEvent> events = new ArrayList<>();
|
||||
|
||||
ListAppender() {
|
||||
super("TestListAppender", null, null, false);
|
||||
// Set filter and layout to ensure events are processed
|
||||
ListAppender(String name) {
|
||||
super(name, null, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(org.apache.logging.log4j.core.LogEvent event) {
|
||||
// Copy event to ensure it's captured even if the original is reused
|
||||
this.events.add(event);
|
||||
public void append(LogEvent event) {
|
||||
// Store an immutable copy to prevent issues with Log4j2 event object reuse
|
||||
events.add(event.toImmutable());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,8 +127,12 @@ class TestLogCapture {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<org.apache.logging.log4j.core.LogEvent> getEvents() {
|
||||
return new ArrayList<>(this.events);
|
||||
List<LogEvent> getEvents() {
|
||||
return new ArrayList<>(events);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
events.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user