using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.AirGap.Importer.Quarantine; namespace StellaOps.AirGap.Importer.Tests.Quarantine; public sealed class FileSystemQuarantineServiceTests { [Fact] public async Task QuarantineAsync_ShouldCreateExpectedFiles_AndListAsyncShouldReturnEntry() { var root = CreateTempDirectory(); try { var bundlePath = Path.Combine(root, "bundle.tar.zst"); await File.WriteAllTextAsync(bundlePath, "bundle-bytes"); var options = Options.Create(new QuarantineOptions { QuarantineRoot = Path.Combine(root, "quarantine"), RetentionPeriod = TimeSpan.FromDays(30), MaxQuarantineSizeBytes = 1024 * 1024, EnableAutomaticCleanup = true }); var svc = new FileSystemQuarantineService( options, NullLogger.Instance, TimeProvider.System); var result = await svc.QuarantineAsync(new QuarantineRequest( TenantId: "tenant-a", BundlePath: bundlePath, ManifestJson: "{\"version\":\"1.0.0\"}", ReasonCode: "dsse:invalid", ReasonMessage: "dsse:invalid", VerificationLog: new[] { "tuf:ok", "dsse:invalid" }, Metadata: new Dictionary { ["k"] = "v" })); result.Success.Should().BeTrue(); Directory.Exists(result.QuarantinePath).Should().BeTrue(); File.Exists(Path.Combine(result.QuarantinePath, "bundle.tar.zst")).Should().BeTrue(); File.Exists(Path.Combine(result.QuarantinePath, "manifest.json")).Should().BeTrue(); File.Exists(Path.Combine(result.QuarantinePath, "verification.log")).Should().BeTrue(); File.Exists(Path.Combine(result.QuarantinePath, "failure-reason.txt")).Should().BeTrue(); File.Exists(Path.Combine(result.QuarantinePath, "quarantine.json")).Should().BeTrue(); var listed = await svc.ListAsync("tenant-a"); listed.Should().ContainSingle(e => e.QuarantineId == result.QuarantineId); } finally { SafeDeleteDirectory(root); } } [Fact] public async Task RemoveAsync_ShouldMoveToRemovedFolder() { var root = CreateTempDirectory(); try { var bundlePath = Path.Combine(root, "bundle.tar.zst"); await File.WriteAllTextAsync(bundlePath, "bundle-bytes"); var quarantineRoot = Path.Combine(root, "quarantine"); var options = Options.Create(new QuarantineOptions { QuarantineRoot = quarantineRoot, MaxQuarantineSizeBytes = 1024 * 1024 }); var svc = new FileSystemQuarantineService(options, NullLogger.Instance, TimeProvider.System); var result = await svc.QuarantineAsync(new QuarantineRequest( TenantId: "tenant-a", BundlePath: bundlePath, ManifestJson: null, ReasonCode: "tuf:invalid", ReasonMessage: "tuf:invalid", VerificationLog: new[] { "tuf:invalid" })); var removed = await svc.RemoveAsync("tenant-a", result.QuarantineId, "investigated"); removed.Should().BeTrue(); Directory.Exists(result.QuarantinePath).Should().BeFalse(); Directory.Exists(Path.Combine(quarantineRoot, "tenant-a", ".removed", result.QuarantineId)).Should().BeTrue(); } finally { SafeDeleteDirectory(root); } } [Fact] public async Task CleanupExpiredAsync_ShouldDeleteOldEntries() { var root = CreateTempDirectory(); try { var bundlePath = Path.Combine(root, "bundle.tar.zst"); await File.WriteAllTextAsync(bundlePath, "bundle-bytes"); var quarantineRoot = Path.Combine(root, "quarantine"); var options = Options.Create(new QuarantineOptions { QuarantineRoot = quarantineRoot, MaxQuarantineSizeBytes = 1024 * 1024 }); var svc = new FileSystemQuarantineService(options, NullLogger.Instance, TimeProvider.System); var result = await svc.QuarantineAsync(new QuarantineRequest( TenantId: "tenant-a", BundlePath: bundlePath, ManifestJson: null, ReasonCode: "tuf:invalid", ReasonMessage: "tuf:invalid", VerificationLog: new[] { "tuf:invalid" })); var jsonPath = Path.Combine(result.QuarantinePath, "quarantine.json"); var json = await File.ReadAllTextAsync(jsonPath); var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }; var entry = JsonSerializer.Deserialize(json, jsonOptions); entry.Should().NotBeNull(); var oldEntry = entry! with { QuarantinedAt = DateTimeOffset.Parse("1900-01-01T00:00:00Z") }; await File.WriteAllTextAsync(jsonPath, JsonSerializer.Serialize(oldEntry, jsonOptions)); var removed = await svc.CleanupExpiredAsync(TimeSpan.FromDays(30)); removed.Should().BeGreaterThanOrEqualTo(1); Directory.Exists(result.QuarantinePath).Should().BeFalse(); } finally { SafeDeleteDirectory(root); } } private static string CreateTempDirectory() { var dir = Path.Combine(Path.GetTempPath(), "stellaops-airgap-tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); return dir; } private static void SafeDeleteDirectory(string path) { try { if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); } } catch { // best-effort cleanup } } }