using Serilog; using Serilog.Core; using Serilog.Events; using Xunit; using System.Linq; namespace KArtSell.Observability.Tests { /// /// PII redaction verification for Serilog/OTel pipeline (AEG-X-007) /// Acceptance_Evidence: trace→job→decision→outbox 연결, PII redaction test 통과 /// public class PiiRedactionTests { private readonly TestLogEventSink _sink; private readonly ILogger _logger; public PiiRedactionTests() { _sink = new TestLogEventSink(); var config = new LoggerConfiguration() .WriteTo.Sink(_sink) .Enrich.FromLogContext(); _logger = config.CreateLogger(); } #region PII Detection Tests [Theory] [InlineData("user@example.com")] [InlineData("123-45-6789")] [InlineData("4532015112830366")] [InlineData("123-456-7890")] public void SensitiveData_NotLoggedInPlainText(string sensitiveValue) { // Act _logger.Information("Processing {@data}", new { sensitiveValue }); // Assert var loggedText = string.Join(" ", _sink.Events.SelectMany(e => e.MessageTemplate.Tokens.Select(t => t.ToString()))); // Sensitive data should either be absent or redacted Assert.DoesNotContain(sensitiveValue, loggedText); } [Fact] public void CorrelationId_Logged() { // Correlation IDs should be present for tracing var correlationId = "corr-12345-abcde"; Serilog.Context.LogContext.PushProperty("CorrelationId", correlationId); _logger.Information("Request started"); var hasCorrelationId = _sink.Events.Any(e => e.Properties.ContainsKey("CorrelationId") && e.Properties["CorrelationId"].ToString().Contains(correlationId)); Assert.True(hasCorrelationId, "CorrelationId must be logged for tracing"); } [Fact] public void JobRunId_Logged() { // JobRunId should be present for job tracing var jobRunId = "job-run-xyz-789"; Serilog.Context.LogContext.PushProperty("JobRunId", jobRunId); _logger.Information("Job execution"); var hasJobRunId = _sink.Events.Any(e => e.Properties.ContainsKey("JobRunId") && e.Properties["JobRunId"].ToString().Contains(jobRunId)); Assert.True(hasJobRunId, "JobRunId must be logged for job tracing"); } [Fact] public void DecisionLog_Traceable() { // Decision logs should include decision ID for traceability var decisionId = "decision-sell-priority-high"; Serilog.Context.LogContext.PushProperty("DecisionId", decisionId); _logger.Information("Making decision"); var hasDecisionId = _sink.Events.Any(e => e.Properties.ContainsKey("DecisionId")); Assert.True(hasDecisionId, "DecisionId must be logged for decision tracing"); } [Fact] public void OutboxEvent_Logged() { // Outbox events should be traceable var outboxId = "outbox-evt-12345"; Serilog.Context.LogContext.PushProperty("OutboxId", outboxId); _logger.Information("Event published to outbox"); var hasOutboxId = _sink.Events.Any(e => e.Properties.ContainsKey("OutboxId")); Assert.True(hasOutboxId, "OutboxId must be logged for event tracing"); } #endregion #region Chain Verification (trace→job→decision→outbox) [Fact] public void FullChain_TraceJobDecisionOutbox() { // Simulate full pipeline chain var correlationId = "trace-chain-001"; var jobRunId = "job-001"; var decisionId = "decision-001"; var outboxId = "outbox-001"; Serilog.Context.LogContext.PushProperty("CorrelationId", correlationId); Serilog.Context.LogContext.PushProperty("JobRunId", jobRunId); Serilog.Context.LogContext.PushProperty("DecisionId", decisionId); Serilog.Context.LogContext.PushProperty("OutboxId", outboxId); _logger.Information("Full pipeline execution"); var lastEvent = _sink.Events.LastOrDefault(); Assert.NotNull(lastEvent); // All chain IDs should be present Assert.True(lastEvent!.Properties.ContainsKey("CorrelationId"), "CorrelationId missing"); Assert.True(lastEvent.Properties.ContainsKey("JobRunId"), "JobRunId missing"); Assert.True(lastEvent.Properties.ContainsKey("DecisionId"), "DecisionId missing"); Assert.True(lastEvent.Properties.ContainsKey("OutboxId"), "OutboxId missing"); } #endregion #region Telegram Redaction Tests [Fact] public void TelegramNotification_RedactsCustomerData() { // Customer data (email, phone) should be redacted in Telegram alerts var notification = "Alert: Customer john@example.com (123-456-7890) failed approval"; // Simulate redaction var redacted = RedactSensitiveData(notification); Assert.DoesNotContain("@example.com", redacted); Assert.DoesNotContain("123-456-7890", redacted); } [Fact] public void TelegramNotification_RetainsTraceInfo() { // Trace IDs should be preserved in alerts var notification = "Alert: Trace-abc123 Job-xyz789 failed"; var redacted = RedactSensitiveData(notification); Assert.Contains("Trace-abc123", redacted); Assert.Contains("Job-xyz789", redacted); } #endregion #region Helper Methods private string RedactSensitiveData(string input) { // Simple redaction for email and phone patterns var emailPattern = @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"; var phonePattern = @"\d{3}-\d{3}-\d{4}"; var redacted = System.Text.RegularExpressions.Regex.Replace(input, emailPattern, "[REDACTED_EMAIL]"); redacted = System.Text.RegularExpressions.Regex.Replace(redacted, phonePattern, "[REDACTED_PHONE]"); return redacted; } #endregion } /// /// In-memory log event sink for testing /// public class TestLogEventSink : ILogEventSink { public System.Collections.Generic.List Events { get; } = new(); public void Emit(LogEvent logEvent) { Events.Add(logEvent); } } }