340 lines
30 KiB
C#
340 lines
30 KiB
C#
using System.Security.Cryptography;
|
|
using System.IO.Compression;
|
|
using Ludarium.Application;
|
|
using Ludarium.Domain;
|
|
using Ludarium.Infrastructure;
|
|
|
|
namespace Ludarium.IntegrationTests;
|
|
|
|
public sealed class SafeScanTests
|
|
{
|
|
[Fact]
|
|
public async Task CueWithExactEcmSidecarIsReviewableButNotReportedMissing()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "ludarium-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
await File.WriteAllTextAsync(Path.Combine(path, "disc.cue"), "FILE \"disc.bin\" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n");
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "disc.bin.ecm"), "ECM\0synthetic-fixture"u8.ToArray());
|
|
var before = Manifest(path);
|
|
var store = new MemoryStore();
|
|
var root = LibraryRoot.Create("PSX", path, LibraryKind.DiscImage);
|
|
await store.UpsertLibraryAsync(root, CancellationToken.None);
|
|
var coordinator = new ScanCoordinator(store, new ReadOnlyLibraryFileSystem());
|
|
|
|
var scan = await coordinator.RequestAsync(root.Id, ScanMode.Deep, "ecm-sidecar", CancellationToken.None);
|
|
await coordinator.ExecuteAsync(scan.Id, CancellationToken.None);
|
|
|
|
var bundle = Assert.Single(await store.ListBundlesAsync(CancellationToken.None));
|
|
Assert.Equal(BundleState.CompleteWithWarnings, bundle.State);
|
|
Assert.Contains(bundle.Members, member => member.Role == "encoded track");
|
|
Assert.DoesNotContain(bundle.Findings, finding => finding.Contains("not found", StringComparison.OrdinalIgnoreCase));
|
|
Assert.DoesNotContain(await store.ListReviewsAsync(CancellationToken.None), item => item.Reason == "Incomplete bundle");
|
|
Assert.Equal(before, Manifest(path));
|
|
}
|
|
finally { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DormantLibraryCannotBeScanned()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "ludarium-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "game.nes"), [0x4e, 0x45, 0x53, 0x1a]);
|
|
var store = new MemoryStore();
|
|
var root = LibraryRoot.Create("Dormant", path, LibraryKind.Rom) with { Enabled = false };
|
|
await store.UpsertLibraryAsync(root, CancellationToken.None);
|
|
var coordinator = new ScanCoordinator(store, new ReadOnlyLibraryFileSystem());
|
|
|
|
var scan = await coordinator.RequestAsync(root.Id, ScanMode.Quick, "dormant", CancellationToken.None);
|
|
await coordinator.ExecuteAsync(scan.Id, CancellationToken.None);
|
|
|
|
var completed = await store.GetScanAsync(scan.Id, CancellationToken.None);
|
|
Assert.Equal(ScanState.Failed, completed?.State);
|
|
Assert.Contains("dormant", completed?.Error, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Empty((await store.SearchArtifactsAsync(null, 1, 10, CancellationToken.None)).Items);
|
|
}
|
|
finally { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeepScanPreservesSourceAndBuildsReviewableWindowsBundle()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "ludarium-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "setup.exe"), "MZ-invalid-static-fixture"u8.ToArray());
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "setup-1.bin"), "payload-one"u8.ToArray());
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "setup-3.bin"), "payload-three"u8.ToArray());
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "sample.nes"), [0x4e, 0x45, 0x53, 0x1a, 0, 0, 0, 0]);
|
|
Directory.CreateDirectory(Path.Combine(path, "roms", "Tools", "Switch"));
|
|
await File.WriteAllTextAsync(Path.Combine(path, "roms", "Tools", "Switch", "prod.keys"), "synthetic-key-list");
|
|
var before = Manifest(path);
|
|
var store = new MemoryStore(); var root = LibraryRoot.Create("Synthetic", path, LibraryKind.Mixed, hashPolicy: HashPolicy.CatalogCompatible);
|
|
await store.UpsertLibraryAsync(root, CancellationToken.None);
|
|
var coordinator = new ScanCoordinator(store, new ReadOnlyLibraryFileSystem());
|
|
var scan = await coordinator.RequestAsync(root.Id, ScanMode.Deep, "safe-scan", CancellationToken.None);
|
|
await coordinator.ExecuteAsync(scan.Id, CancellationToken.None);
|
|
Assert.Equal(before, Manifest(path));
|
|
Assert.Equal(ScanState.Completed, (await store.GetScanAsync(scan.Id, CancellationToken.None))?.State);
|
|
var artifacts = await store.SearchArtifactsAsync(null, 1, 50, CancellationToken.None);
|
|
Assert.Equal(5, artifacts.Total);
|
|
Assert.Contains(artifacts.Items, item => item.State == ArtifactState.Ignored && item.MediaType == MediaType.SupportFile);
|
|
var bundle = Assert.Single(await store.ListBundlesAsync(CancellationToken.None));
|
|
Assert.Equal(BundleKind.WindowsInstaller, bundle.Kind); Assert.Equal(BundleState.Incomplete, bundle.State);
|
|
Assert.Contains(bundle.Findings, x => x.Contains("payload 2", StringComparison.Ordinal));
|
|
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), x => x.Reason == "Incomplete bundle");
|
|
Assert.Equal(["ArtworkEnrichment", "MetadataEnrichment"], (await store.ListJobsAsync(CancellationToken.None)).Select(job => job.Kind).Order().ToArray());
|
|
}
|
|
finally { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifiedRenameRetainsArtifactIdentityAndUnavailableRootDoesNotRemoveIt()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "ludarium-tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "before.nes"), [0x4e, 0x45, 0x53, 0x1a, 1, 2, 3, 4]);
|
|
var store = new MemoryStore(); var root = LibraryRoot.Create("Moves", path, LibraryKind.Rom); await store.UpsertLibraryAsync(root, CancellationToken.None);
|
|
var coordinator = new ScanCoordinator(store, new ReadOnlyLibraryFileSystem());
|
|
var first = await coordinator.RequestAsync(root.Id, ScanMode.Deep, "first", CancellationToken.None); await coordinator.ExecuteAsync(first.Id, CancellationToken.None);
|
|
var original = Assert.Single((await store.SearchArtifactsAsync(null, 1, 10, CancellationToken.None)).Items);
|
|
File.Move(Path.Combine(path, "before.nes"), Path.Combine(path, "after.nes"));
|
|
var second = await coordinator.RequestAsync(root.Id, ScanMode.Deep, "second", CancellationToken.None); await coordinator.ExecuteAsync(second.Id, CancellationToken.None);
|
|
var moved = Assert.Single((await store.SearchArtifactsAsync(null, 1, 10, CancellationToken.None)).Items);
|
|
Assert.Equal(original.Id, moved.Id); Assert.Equal("after.nes", moved.RelativePath); Assert.Equal(ArtifactState.Present, moved.State);
|
|
Assert.Equal(1, (await store.GetScanAsync(second.Id, CancellationToken.None))?.MovedItems);
|
|
Directory.Move(path, path + "-offline");
|
|
var failed = await coordinator.RequestAsync(root.Id, ScanMode.Quick, "offline", CancellationToken.None); await coordinator.ExecuteAsync(failed.Id, CancellationToken.None);
|
|
Assert.Equal(ArtifactState.Present, Assert.Single((await store.SearchArtifactsAsync(null, 1, 10, CancellationToken.None)).Items).State);
|
|
Directory.Move(path + "-offline", path);
|
|
}
|
|
finally { if (Directory.Exists(path)) Directory.Delete(path, true); if (Directory.Exists(path + "-offline")) Directory.Delete(path + "-offline", true); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmptyMountedSourceDoesNotEraseAnExistingCatalog()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "ludarium-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "preserved.nes"), [0x4e, 0x45, 0x53, 0x1a, 1, 2, 3, 4]);
|
|
var store = new MemoryStore();
|
|
var root = LibraryRoot.Create("Protected", path, LibraryKind.Rom);
|
|
await store.UpsertLibraryAsync(root, CancellationToken.None);
|
|
var coordinator = new ScanCoordinator(store, new ReadOnlyLibraryFileSystem());
|
|
var initial = await coordinator.RequestAsync(root.Id, ScanMode.Quick, "initial", CancellationToken.None);
|
|
await coordinator.ExecuteAsync(initial.Id, CancellationToken.None);
|
|
|
|
File.Delete(Path.Combine(path, "preserved.nes"));
|
|
var empty = await coordinator.RequestAsync(root.Id, ScanMode.Quick, "empty", CancellationToken.None);
|
|
await coordinator.ExecuteAsync(empty.Id, CancellationToken.None);
|
|
|
|
var result = Assert.Single((await store.SearchArtifactsAsync(null, 1, 10, CancellationToken.None)).Items);
|
|
Assert.Equal(ArtifactState.Present, result.State);
|
|
Assert.Equal(ScanState.Failed, (await store.GetScanAsync(empty.Id, CancellationToken.None))?.State);
|
|
Assert.Contains("source is empty", (await store.GetScanAsync(empty.Id, CancellationToken.None))?.Error, StringComparison.OrdinalIgnoreCase);
|
|
Assert.False((await store.GetLibraryAsync(root.Id, CancellationToken.None))?.HasEntries);
|
|
}
|
|
finally { if (Directory.Exists(path)) Directory.Delete(path, true); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IntegrityScanHashesOnDemandLibrariesWithoutMutatingTheSource()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "ludarium-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
await File.WriteAllBytesAsync(Path.Combine(path, "identity.nes"), [0x4e, 0x45, 0x53, 0x1a, 1, 2, 3, 4]);
|
|
var before = Manifest(path);
|
|
var store = new MemoryStore(); var root = LibraryRoot.Create("On demand", path, LibraryKind.Rom, hashPolicy: HashPolicy.OnDemand);
|
|
await store.UpsertLibraryAsync(root, CancellationToken.None);
|
|
var coordinator = new ScanCoordinator(store, new ReadOnlyLibraryFileSystem());
|
|
var scan = await coordinator.RequestAsync(root.Id, ScanMode.Integrity, "integrity", CancellationToken.None);
|
|
await coordinator.ExecuteAsync(scan.Id, CancellationToken.None);
|
|
var artifact = Assert.Single((await store.SearchArtifactsAsync(null, 1, 10, CancellationToken.None)).Items);
|
|
Assert.NotNull(artifact.Sha256);
|
|
Assert.Equal(before, Manifest(path));
|
|
}
|
|
finally { if (Directory.Exists(path)) Directory.Delete(path, true); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeepScanRecordsZipMembersWithoutChangingTheArchive()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "ludarium-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
var archivePath = Path.Combine(path, "collection.zip");
|
|
await using (var output = File.Create(archivePath))
|
|
using (var archive = new ZipArchive(output, ZipArchiveMode.Create))
|
|
{
|
|
var entry = archive.CreateEntry("game/demo.nes");
|
|
await using var member = entry.Open();
|
|
await member.WriteAsync(new byte[] { 0x4e, 0x45, 0x53, 0x1a });
|
|
}
|
|
var before = Manifest(path);
|
|
var store = new MemoryStore();
|
|
var root = LibraryRoot.Create("Archives", path, LibraryKind.Mixed, inspectArchives: true);
|
|
await store.UpsertLibraryAsync(root, CancellationToken.None);
|
|
var coordinator = new ScanCoordinator(store, new ReadOnlyLibraryFileSystem());
|
|
|
|
var scan = await coordinator.RequestAsync(root.Id, ScanMode.Deep, "zip", CancellationToken.None);
|
|
await coordinator.ExecuteAsync(scan.Id, CancellationToken.None);
|
|
|
|
var artifact = Assert.Single((await store.SearchArtifactsAsync(null, 1, 10, CancellationToken.None)).Items);
|
|
var memberRecord = Assert.Single(await store.ListArchiveMembersAsync(artifact.Id, CancellationToken.None));
|
|
Assert.Equal("game/demo.nes", memberRecord.Path);
|
|
Assert.Equal(before, Manifest(path));
|
|
}
|
|
finally { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InventoryExportsAreVersionedEscapedAndCancellationSafe()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "ludarium-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
try
|
|
{
|
|
var store = new MemoryStore();
|
|
var artifact = new Artifact(Guid.NewGuid(), Guid.NewGuid(), "quoted,\nname.nes", 4, DateTimeOffset.UtcNow,
|
|
ArtifactState.Present, MediaType.Rom, Confidence.Deterministic, Guid.NewGuid(), DateTimeOffset.UtcNow,
|
|
DateTimeOffset.UtcNow, "iNES", "nes", Sha256: new string('a', 64));
|
|
await store.UpsertArtifactAsync(artifact, null, [], CancellationToken.None);
|
|
var service = new InventoryExportService(store, path);
|
|
|
|
foreach (var format in new[] { "json", "csv", "sha256" })
|
|
{
|
|
var result = await service.CreateAsync(format, CancellationToken.None);
|
|
var content = await File.ReadAllTextAsync(Path.Combine(path, result.FileName));
|
|
Assert.Contains(ReleaseIdentity.Version, content, StringComparison.Ordinal);
|
|
Assert.Contains(format == "json" ? "schemaVersion" : "ludarium-schema-version", content, StringComparison.Ordinal);
|
|
if (format == "sha256") Assert.Contains("quoted,\\nname.nes", content, StringComparison.Ordinal);
|
|
Assert.Equal(1, result.Count);
|
|
}
|
|
|
|
var cancelledRoot = Path.Combine(path, "cancelled");
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
|
new InventoryExportService(store, cancelledRoot).CreateAsync("json", cancellation.Token));
|
|
Assert.Empty(Directory.GetFiles(cancelledRoot));
|
|
}
|
|
finally { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); }
|
|
}
|
|
|
|
private static string Manifest(string root) => string.Join('|', Directory.GetFiles(root).Order().Select(x => Path.GetFileName(x) + ":" + Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(x)))));
|
|
|
|
private sealed class MemoryStore : ILudariumStore
|
|
{
|
|
private readonly Dictionary<Guid, LibraryRoot> libraries = []; private readonly Dictionary<Guid, ScanRun> scans = []; private readonly Dictionary<Guid, Artifact> artifacts = []; private readonly Dictionary<Guid, Bundle> bundles = []; private readonly Dictionary<Guid, ReviewItem> reviews = []; private readonly Dictionary<Guid, IReadOnlyList<ArchiveMember>> archiveMembers = []; private readonly List<Finding> findings = []; private readonly List<BackgroundJob> jobs = [];
|
|
private OperationalSettings settings = OperationalSettings.Defaults;
|
|
public Task InitializeAsync(CancellationToken c) => Task.CompletedTask;
|
|
public Task<int> GetSchemaVersionAsync(CancellationToken c) => Task.FromResult(PostgresStore.CurrentSchemaVersion);
|
|
public Task<IReadOnlyList<LibraryRoot>> ListLibrariesAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<LibraryRoot>>(libraries.Values.ToArray());
|
|
public Task<LibraryRoot?> GetLibraryAsync(Guid id, CancellationToken c) => Task.FromResult(libraries.GetValueOrDefault(id));
|
|
public Task<IReadOnlyList<LibraryHealthSummary>> ListLibraryHealthAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<LibraryHealthSummary>>([]);
|
|
public Task UpsertLibraryAsync(LibraryRoot r, CancellationToken c) { libraries[r.Id] = r; return Task.CompletedTask; }
|
|
public Task<ScanRun> CreateScanAsync(Guid libraryId, ScanMode mode, string? key, CancellationToken c) { var existing = scans.Values.FirstOrDefault(x => x.Checkpoint == "key:" + key); if (existing is not null) return Task.FromResult(existing); var s = new ScanRun(Guid.NewGuid(), libraryId, mode, ScanState.Queued, "Queued", 0, null, 0, null, "key:" + key, false, DateTimeOffset.UtcNow); scans[s.Id] = s; return Task.FromResult(s); }
|
|
public Task<IReadOnlyList<ScanRun>> ListScansAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<ScanRun>>(scans.Values.ToArray());
|
|
public Task<ScanRun?> GetScanAsync(Guid id, CancellationToken c) => Task.FromResult(scans.GetValueOrDefault(id));
|
|
public Task UpdateScanAsync(ScanRun s, CancellationToken c) { scans[s.Id] = s; return Task.CompletedTask; }
|
|
public Task UpsertArtifactAsync(Artifact a, ContentBlob? b, IReadOnlyList<Evidence> e, CancellationToken c) { artifacts[a.Id] = a; return Task.CompletedTask; }
|
|
public Task MarkArtifactIgnoredAsync(Guid id, long version, Evidence evidence, CancellationToken c) { artifacts[id] = artifacts[id] with { State = ArtifactState.Ignored, MediaType = MediaType.SupportFile, Platform = null, Version = version }; return Task.CompletedTask; }
|
|
public Task<ArtifactOverride?> GetArtifactOverrideAsync(Guid artifactId, CancellationToken c) => Task.FromResult<ArtifactOverride?>(null);
|
|
public Task<Artifact?> FindByPathAsync(Guid l, string p, CancellationToken c) => Task.FromResult(artifacts.Values.FirstOrDefault(x => x.LibraryId == l && x.RelativePath.Equals(p, StringComparison.OrdinalIgnoreCase)));
|
|
public Task<Artifact?> FindByHashAsync(Guid l, long z, string h, CancellationToken c) => Task.FromResult(artifacts.Values.FirstOrDefault(x => x.LibraryId == l && x.Size == z && x.Sha256 == h));
|
|
public Task<long> MarkMissingExceptAsync(Guid l, IReadOnlySet<string> paths, DateTimeOffset at, CancellationToken c) { var missing = artifacts.Values.Where(x => x.LibraryId == l && x.State == ArtifactState.Present && !paths.Contains(x.RelativePath)).ToArray(); foreach (var a in missing) artifacts[a.Id] = a with { State = ArtifactState.Missing }; return Task.FromResult((long)missing.Length); }
|
|
public Task<Page<Artifact>> SearchArtifactsAsync(string? q, int p, int z, CancellationToken c) { var x = artifacts.Values.Where(a => q is null || a.RelativePath.Contains(q, StringComparison.OrdinalIgnoreCase)).ToArray(); return Task.FromResult(new Page<Artifact>(x, p, z, x.Length)); }
|
|
public Task<ArtifactDetails?> GetArtifactDetailsAsync(Guid id, CancellationToken c) => Task.FromResult<ArtifactDetails?>(null);
|
|
public Task<DashboardSummary> GetDashboardSummaryAsync(CancellationToken c) => Task.FromResult(new DashboardSummary(libraries.Count, artifacts.Count, artifacts.Count, 0, 0, bundles.Count, artifacts.Values.Sum(x => x.Size), artifacts.Values.LongCount(x => x.Sha256 is not null), artifacts.Values.LongCount(x => x.MediaType != MediaType.Unknown), artifacts.Values.LongCount(x => x.MediaType == MediaType.Unknown), artifacts.Values.LongCount(x => x.State == ArtifactState.Missing), 0, 0, reviews.Values.LongCount(x => x.State != ReviewState.Resolved), findings.LongCount(x => x.Severity == Severity.Critical), scans.Values.LongCount(x => x.State is not (ScanState.Completed or ScanState.Failed or ScanState.Cancelled)), 0));
|
|
public Task<StorageSummary> GetStorageSummaryAsync(CancellationToken c)
|
|
{
|
|
var summaries = libraries.Values.Select(library =>
|
|
{
|
|
var stored = artifacts.Values.Where(item => item.LibraryId == library.Id).ToArray();
|
|
return new LibraryStorageSummary(library.Id, library.Name, stored.LongLength,
|
|
stored.Where(item => item.State == ArtifactState.Present).Sum(item => item.Size),
|
|
stored.LongCount(item => item.State == ArtifactState.Missing));
|
|
}).ToArray();
|
|
return Task.FromResult(new StorageSummary(artifacts.Values.Sum(x => x.Size), 0, [], summaries));
|
|
}
|
|
public Task<HealthSummary> GetHealthSummaryAsync(CancellationToken c) => Task.FromResult(new HealthSummary(0, 0, 0, 0, 0, 0, findings));
|
|
public Task<IReadOnlyList<Artifact>> GetDuplicatesAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<Artifact>>(artifacts.Values.GroupBy(x => x.Sha256).Where(x => x.Key is not null && x.Count() > 1).SelectMany(x => x).ToArray());
|
|
public Task ReplaceArchiveMembersAsync(Guid artifactId, IReadOnlyList<ArchiveMember> members, CancellationToken c) { archiveMembers[artifactId] = members; return Task.CompletedTask; }
|
|
public Task<IReadOnlyList<ArchiveMember>> ListArchiveMembersAsync(Guid artifactId, CancellationToken c) => Task.FromResult(archiveMembers.GetValueOrDefault(artifactId) ?? []);
|
|
public Task AddBundleAsync(Bundle b, CancellationToken c) { bundles[b.Id] = b; return Task.CompletedTask; }
|
|
public Task<IReadOnlyList<Bundle>> ListBundlesAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<Bundle>>(bundles.Values.ToArray());
|
|
public Task AddReviewAsync(ReviewItem r, CancellationToken c) { reviews.TryAdd(r.Id, r); return Task.CompletedTask; }
|
|
public Task<IReadOnlyList<ReviewItem>> ListReviewsAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<ReviewItem>>(reviews.Values.ToArray());
|
|
public Task<Page<ReviewItem>> SearchReviewsAsync(ReviewState? state, string? reason, int p, int z, CancellationToken c) { var x = reviews.Values.Where(r => (state is null || r.State == state) && (reason is null || r.Reason == reason)).ToArray(); return Task.FromResult(new Page<ReviewItem>(x, p, z, x.Length)); }
|
|
public Task<ReviewItem> SetReviewStateAsync(Guid id, ReviewState state, string actor, CancellationToken c) { reviews[id] = reviews[id] with { State = state }; return Task.FromResult(reviews[id]); }
|
|
public Task<ManualResolution> ResolveReviewAsync(Guid id, string r, string a, CancellationToken c) { var before = reviews[id]; reviews[id] = before with { State = ReviewState.Resolved, Resolution = r }; return Task.FromResult(new ManualResolution(Guid.NewGuid(), id, r, a, System.Text.Json.JsonSerializer.Serialize(before), System.Text.Json.JsonSerializer.Serialize(reviews[id]), DateTimeOffset.UtcNow)); }
|
|
public Task UndoResolutionAsync(Guid id, string actor, CancellationToken c) => Task.CompletedTask;
|
|
public Task AddClaimAsync(MetadataClaim claim, CancellationToken c) => Task.CompletedTask;
|
|
public Task<IReadOnlyList<MetadataClaim>> ListClaimsAsync(string entityType, Guid entityId, CancellationToken c) => Task.FromResult<IReadOnlyList<MetadataClaim>>([]);
|
|
public Task<Game> CreateGameAsync(string title, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<GameDeletionResult> DeleteGameAsync(Guid id, long expectedVersion, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<Game> UpdateGameAsync(Guid id, string title, long version, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<Game?> GetGameAsync(Guid id, CancellationToken c) => Task.FromResult<Game?>(null);
|
|
public Task<IReadOnlyList<Game>> ListGamesAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<Game>>([]);
|
|
public Task<Page<Game>> SearchGamesAsync(GameQuery query, CancellationToken c) =>
|
|
Task.FromResult(new Page<Game>([], query.Page, query.PageSize, 0));
|
|
public Task<IReadOnlyList<Release>> ListReleasesForGamesAsync(IReadOnlyList<Guid> gameIds, CancellationToken c) =>
|
|
Task.FromResult<IReadOnlyList<Release>>([]);
|
|
public Task<Page<WishlistItem>> SearchWishlistAsync(string? q, string? platform, WishlistPriority? priority, WishlistStatus? status, string? sort, int page, int pageSize, CancellationToken c) => Task.FromResult(new Page<WishlistItem>([], page, pageSize, 0));
|
|
public Task<WishlistSummary> GetWishlistSummaryAsync(CancellationToken c) => Task.FromResult(new WishlistSummary(0, 0, 0, 0, 0));
|
|
public Task<WishlistItem> CreateWishlistItemAsync(WishlistInput input, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<WishlistItem> UpdateWishlistItemAsync(Guid id, WishlistInput input, long expectedVersion, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task DeleteWishlistItemAsync(Guid id, long expectedVersion, string actor, CancellationToken c) => Task.CompletedTask;
|
|
public Task<int> ReconcileWishlistAsync(string actor, CancellationToken c) => Task.FromResult(0);
|
|
public Task<IReadOnlyList<ReviewGroup>> ListReviewGroupsAsync(ReviewState state, CancellationToken c) => Task.FromResult<IReadOnlyList<ReviewGroup>>([]);
|
|
public Task<ReviewOperation> ResolveReviewGroupAsync(IReadOnlyList<Guid> ids, int expectedCount, string resolution, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<ReviewOperation> ApplyReviewGroupActionAsync(IReadOnlyList<Guid> ids, int expectedCount, ReviewBulkAction action, string? platform, string? pathPattern, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<ReviewOperation> DeferReviewGroupAsync(IReadOnlyList<Guid> ids, int expectedCount, string reason, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<ReviewOperation> ReopenReviewGroupAsync(IReadOnlyList<Guid> ids, int expectedCount, ReviewState sourceState, string reason, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task UndoReviewOperationAsync(Guid id, string actor, CancellationToken c) => Task.CompletedTask;
|
|
public Task<int> SynchronizeDiscoveredGamesAsync(Guid libraryId, CancellationToken c) => Task.FromResult(0);
|
|
public Task UpsertGameArtworkAsync(GameArtwork artwork, CancellationToken c) => Task.CompletedTask;
|
|
public Task<GameArtwork?> GetSelectedGameArtworkAsync(Guid gameId, CancellationToken c, string? platform = null) => Task.FromResult<GameArtwork?>(null);
|
|
public Task<IReadOnlyList<GameArtwork>> ListGameArtworkAsync(Guid gameId, CancellationToken c) => Task.FromResult<IReadOnlyList<GameArtwork>>([]);
|
|
public Task SelectGameArtworkAsync(Guid gameId, Guid artworkId, CancellationToken c) => Task.CompletedTask;
|
|
public Task<IReadOnlyList<GameArtwork>> ListArtworkNeedingReviewAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<GameArtwork>>([]);
|
|
public Task<IReadOnlyList<ArtworkReviewItem>> ListArtworkReviewItemsAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<ArtworkReviewItem>>([]);
|
|
public Task<BackgroundJob> EnqueueJobAsync(string kind, string payload, string? key, CancellationToken c)
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
var job = new BackgroundJob(Guid.NewGuid(), kind, JobState.Queued, payload, 0, null, 0, 3, now, now, now);
|
|
jobs.Add(job);
|
|
return Task.FromResult(job);
|
|
}
|
|
public Task<BackgroundJob?> LeaseJobAsync(string worker, TimeSpan lease, CancellationToken c) => Task.FromResult<BackgroundJob?>(null);
|
|
public Task UpdateJobAsync(BackgroundJob job, CancellationToken c) => Task.CompletedTask;
|
|
public Task<IReadOnlyList<BackgroundJob>> ListJobsAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<BackgroundJob>>(jobs);
|
|
public Task<BackgroundJob?> GetJobAsync(Guid id, CancellationToken c) => Task.FromResult<BackgroundJob?>(null);
|
|
public Task RequestJobCancellationAsync(Guid id, CancellationToken c) => Task.CompletedTask;
|
|
public Task<BackgroundJob> RetryJobAsync(Guid id, CancellationToken c) => throw new NotSupportedException();
|
|
public Task UpsertProviderSnapshotAsync(ProviderSnapshot snapshot, CancellationToken c) => Task.CompletedTask;
|
|
public Task<IReadOnlyList<ProviderSnapshot>> ListProviderSnapshotsAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<ProviderSnapshot>>([]);
|
|
public Task<IReadOnlyList<PlatformSummary>> ListPlatformsAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<PlatformSummary>>([]);
|
|
public Task<Release> CreateReleaseAsync(Guid gameId, string title, string? platform, string? region, string? revision, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<Release> UpdateReleaseAsync(Guid id, string title, string? platform, string? region, string? revision, long expectedVersion, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task DeleteReleaseAsync(Guid id, long expectedVersion, string actor, CancellationToken c) => throw new NotSupportedException();
|
|
public Task<IReadOnlyList<Release>> ListReleasesAsync(Guid? gameId, CancellationToken c) => Task.FromResult<IReadOnlyList<Release>>([]);
|
|
public Task<ArtifactDetails?> GetGameArtworkAsync(Guid gameId, CancellationToken c) => Task.FromResult<ArtifactDetails?>(null);
|
|
public Task<IReadOnlyList<Finding>> ListFindingsAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<Finding>>(findings); public Task AddFindingAsync(Finding f, CancellationToken c) { findings.Add(f); return Task.CompletedTask; }
|
|
public Task<IntegritySnapshot> CreateSnapshotAsync(string n, CancellationToken c) => throw new NotSupportedException(); public Task<IReadOnlyList<IntegritySnapshot>> ListSnapshotsAsync(CancellationToken c) => Task.FromResult<IReadOnlyList<IntegritySnapshot>>([]); public Task<IntegritySnapshot?> GetSnapshotAsync(Guid id, CancellationToken c) => Task.FromResult<IntegritySnapshot?>(null);
|
|
public Task<OperationalSettings> GetOperationalSettingsAsync(CancellationToken c) => Task.FromResult(settings);
|
|
public Task<OperationalSettings> SaveOperationalSettingsAsync(OperationalSettings value, long expectedVersion, CancellationToken c) { settings = value with { Version = expectedVersion + 1 }; return Task.FromResult(settings); }
|
|
}
|
|
}
|