using Dinah.Core.Security;
using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using Serilog;
using System;
using System.IO;
using System.Linq;
namespace SerilogConfigurationTests;
///
/// Builds the logger the way Libation does and writes through it, rather than testing the destructuring policy
/// on its own: the policy only protects anything if actually
/// registers it, and a missing registration is silent.
///
[TestClass]
[DoNotParallelize]
public class MaskedLogEntryLoggingTests
{
private const string Secret = "jade@example.com";
private const string Masked = "AccountId=j[...]e|Locale=us";
private string tempDir = string.Empty;
private ILogger originalLogger = Serilog.Log.Logger;
private class MaskedThing : ILogMasked
{
public string MaskedLogEntry => Masked;
public string Address => Secret;
public override string ToString() => Masked;
}
[TestInitialize]
public void Initialize()
{
originalLogger = Serilog.Log.Logger;
tempDir = Path.Combine(Path.GetTempPath(), $"libation-masked-log-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
}
[TestCleanup]
public void Cleanup()
{
Serilog.Log.CloseAndFlush();
Serilog.Log.Logger = originalLogger;
Configuration.RestoreSingletonInstance();
try
{
Directory.Delete(tempDir, recursive: true);
}
catch (IOException)
{
// A leftover temp directory is not worth failing a test over.
}
}
private string LogThrough(Action write)
{
var config = Configuration.CreateMockInstance();
config.EnsureSerilogConfig();
var args = (JObject)((JObject)config.GetObject("Serilog")!).SelectToken("$.WriteTo[0].Args")!;
args["path"] = Path.Combine(tempDir, "Log.log");
args["outputTemplate"] = "{Message:lj} {Properties:j}{NewLine}";
config.SetNonString((JObject)config.GetObject("Serilog")!, "Serilog");
config.ConfigureLogging();
write(Serilog.Log.Logger);
Serilog.Log.CloseAndFlush();
return string.Join("\n", Directory.GetFiles(tempDir, "Log*.log").Select(File.ReadAllText));
}
/// Destructured, so the policy is what has to catch it: without one, Address would be written.
[TestMethod]
public void a_destructured_masked_type_is_reduced_to_its_masked_entry()
{
var written = LogThrough(logger => logger.Information("scanning {@Account}", new MaskedThing()));
StringAssert.Contains(written, Masked);
Assert.IsFalse(written.Contains(Secret, StringComparison.Ordinal), "the log contained the unmasked value");
}
/// The shape most of Libation's logging uses: an anonymous object holding the thing.
[TestMethod]
public void a_masked_type_nested_in_a_debug_object_is_reduced_too()
{
var written = LogThrough(logger => logger.Information("scanning {@DebugInfo}", new { Account = new MaskedThing(), Attempt = 2 }));
StringAssert.Contains(written, Masked);
Assert.IsFalse(written.Contains(Secret, StringComparison.Ordinal), "the log contained the unmasked value");
}
///
/// Destructured on purpose. Without the transform a secret renders as an empty structure - safe, but it says
/// nothing, and the shape is the whole point of the redaction.
///
[TestMethod]
public void a_destructured_secret_is_written_as_its_shape()
{
var written = LogThrough(logger => logger.Information("key {@Key}", new SecretString(Secret)));
StringAssert.Contains(written, $"[REDACTED length={Secret.Length}]");
Assert.IsFalse(written.Contains(Secret, StringComparison.Ordinal), "the log contained the secret");
}
}