using System.IO.Compression; using System.Reflection; using Ludarium.Application; using Ludarium.Domain; using Ludarium.Infrastructure; namespace Ludarium.IntegrationTests; public sealed class SupportBundleServiceTests { [Fact] public async Task BundleRedactsPathsErrorsAndFindingDiagnosticsByConstruction() { const string secret = "super-secret-token"; var now = DateTimeOffset.UtcNow; var libraryId = Guid.NewGuid(); var store = DispatchProxy.Create(); var proxy = (SupportStoreProxy)(object)store; proxy.Libraries = [new(libraryId, "Games", $"/mnt/{secret}", LibraryKind.Mixed, true, HashPolicy.Sha256, false, 1, true, true, true, true, now)]; proxy.Scans = [new(Guid.NewGuid(), libraryId, ScanMode.Deep, ScanState.Failed, "failed", 1, 1, 10, 10, null, false, now, now, now, $"Host=database;Password={secret}")]; proxy.Findings = [new(Guid.NewGuid(), "Integrity", Severity.Critical, $"API key {secret} found at /mnt/private/game.rom", null, null, now)]; var directory = Path.Combine(Path.GetTempPath(), "ludarium-support-tests", Guid.NewGuid().ToString("N")); try { var result = await new SupportBundleService(store, directory).CreateAsync(CancellationToken.None); using var archive = ZipFile.OpenRead(Path.Combine(directory, result.FileName)); var text = string.Join('\n', archive.Entries.Select(entry => { using var reader = new StreamReader(entry.Open()); return reader.ReadToEnd(); })); Assert.DoesNotContain(secret, text, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("/mnt/private", text, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("Password=", text, StringComparison.OrdinalIgnoreCase); Assert.Contains("[redacted]", text, StringComparison.OrdinalIgnoreCase); } finally { if (Directory.Exists(directory)) Directory.Delete(directory, true); } } public class SupportStoreProxy : DispatchProxy { public IReadOnlyList Libraries { get; set; } = []; public IReadOnlyList Scans { get; set; } = []; public IReadOnlyList Findings { get; set; } = []; protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) => targetMethod?.Name switch { nameof(ILudariumStore.ListLibrariesAsync) => Task.FromResult(Libraries), nameof(ILudariumStore.ListScansAsync) => Task.FromResult(Scans), nameof(ILudariumStore.ListFindingsAsync) => Task.FromResult(Findings), _ => throw new NotSupportedException(targetMethod?.Name) }; } }