This commit is contained in:
@@ -0,0 +1,619 @@
|
||||
using Ludarium.Application;
|
||||
using Ludarium.Domain;
|
||||
using Ludarium.Infrastructure;
|
||||
using Npgsql;
|
||||
using Testcontainers.PostgreSql;
|
||||
|
||||
namespace Ludarium.IntegrationTests;
|
||||
|
||||
public sealed class PostgresStoreTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", "Container")]
|
||||
public async Task MigrationsPersistenceIdempotencyAndOptimisticConcurrencyWorkAgainstPostgres()
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("LUDARIUM_RUN_CONTAINER_TESTS"), "1", StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
await using var postgres = new PostgreSqlBuilder("postgres:16.15-bookworm")
|
||||
.WithDatabase("ludarium_test")
|
||||
.WithUsername("ludarium")
|
||||
.WithPassword("synthetic-test-password")
|
||||
.Build();
|
||||
await postgres.StartAsync();
|
||||
await using var dataSource = NpgsqlDataSource.Create(postgres.GetConnectionString());
|
||||
var store = new PostgresStore(dataSource);
|
||||
|
||||
await store.InitializeAsync(CancellationToken.None);
|
||||
await store.InitializeAsync(CancellationToken.None);
|
||||
Assert.Equal(PostgresStore.CurrentSchemaVersion, await store.GetSchemaVersionAsync(CancellationToken.None));
|
||||
var emptySnapshot = await Assert.ThrowsAsync<SnapshotPreconditionException>(() =>
|
||||
store.CreateSnapshotAsync("empty baseline", CancellationToken.None));
|
||||
Assert.Contains("at least one present file with a SHA-256 hash", emptySnapshot.Message);
|
||||
Assert.Empty(await store.ListSnapshotsAsync(CancellationToken.None));
|
||||
|
||||
await using (var versions = dataSource.CreateCommand("SELECT array_agg(version ORDER BY version) FROM schema_versions"))
|
||||
{
|
||||
var appliedVersions = Assert.IsType<int[]>(await versions.ExecuteScalarAsync());
|
||||
Assert.Collection(appliedVersions,
|
||||
value => Assert.Equal(1, value), value => Assert.Equal(2, value),
|
||||
value => Assert.Equal(3, value), value => Assert.Equal(4, value),
|
||||
value => Assert.Equal(5, value), value => Assert.Equal(6, value),
|
||||
value => Assert.Equal(7, value), value => Assert.Equal(8, value),
|
||||
value => Assert.Equal(9, value), value => Assert.Equal(10, value),
|
||||
value => Assert.Equal(11, value), value => Assert.Equal(12, value),
|
||||
value => Assert.Equal(13, value), value => Assert.Equal(14, value),
|
||||
value => Assert.Equal(15, value), value => Assert.Equal(16, value),
|
||||
value => Assert.Equal(17, value), value => Assert.Equal(18, value),
|
||||
value => Assert.Equal(19, value), value => Assert.Equal(20, value),
|
||||
value => Assert.Equal(21, value), value => Assert.Equal(22, value),
|
||||
value => Assert.Equal(23, value), value => Assert.Equal(24, value),
|
||||
value => Assert.Equal(25, value), value => Assert.Equal(26, value),
|
||||
value => Assert.Equal(27, value));
|
||||
}
|
||||
|
||||
var firmware = new FirmwareAsset(Guid.NewGuid(), "psx", "bios", FirmwareAssetKind.Bios,
|
||||
"personal.bin", 512, new string('f', 64), "psx/bios/personal.bin", true,
|
||||
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow);
|
||||
await store.SaveFirmwareAssetAsync(firmware, "tester", CancellationToken.None);
|
||||
Assert.Equal(firmware.Id, (await store.GetSelectedFirmwareAssetAsync("psx", "bios", CancellationToken.None))?.Id);
|
||||
Assert.Contains(await store.ListFirmwareAssetsAsync(CancellationToken.None), item => item.Sha256 == firmware.Sha256);
|
||||
var removedFirmware = await store.DeleteFirmwareAssetAsync(firmware.Id, firmware.Version, "tester", CancellationToken.None);
|
||||
Assert.Single(removedFirmware);
|
||||
Assert.Null(await store.GetSelectedFirmwareAssetAsync("psx", "bios", CancellationToken.None));
|
||||
|
||||
var library = LibraryRoot.Create("Synthetic", Path.GetFullPath(Path.Combine(Path.GetTempPath(),
|
||||
"ludarium-integration-library")), LibraryKind.Mixed) with
|
||||
{ IsReadOnly = true, IsAvailable = true };
|
||||
await store.UpsertLibraryAsync(library, CancellationToken.None);
|
||||
var firstScan = await store.CreateScanAsync(library.Id, ScanMode.Deep, "stable-request", CancellationToken.None);
|
||||
var repeatedScan = await store.CreateScanAsync(library.Id, ScanMode.Deep, "stable-request", CancellationToken.None);
|
||||
Assert.Equal(firstScan.Id, repeatedScan.Id);
|
||||
|
||||
var content = new ContentBlob(Guid.NewGuid(), 4, new string('a', 64), new string('b', 40), "1234abcd", DateTimeOffset.UtcNow);
|
||||
var artifact = new Artifact(Guid.NewGuid(), library.Id, "roms/nes/Synthetic.nes", 4, DateTimeOffset.UtcNow,
|
||||
ArtifactState.Present, MediaType.Rom, Confidence.Deterministic, content.Id,
|
||||
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, "iNES", "nes", Sha256: content.Sha256);
|
||||
var evidence = new Evidence("signature", "iNES", "test", "1", Confidence.Deterministic, DateTimeOffset.UtcNow);
|
||||
await store.UpsertArtifactAsync(artifact, content, [evidence], CancellationToken.None);
|
||||
await store.UpsertArtifactAsync(artifact with { Version = 2 }, content, [evidence], CancellationToken.None);
|
||||
var persisted = await store.GetArtifactDetailsAsync(artifact.Id, CancellationToken.None);
|
||||
Assert.NotNull(persisted);
|
||||
Assert.Single(persisted.Evidence);
|
||||
Assert.Equal(content.Sha256, persisted.Artifact.Sha256);
|
||||
var integritySnapshot = await store.CreateSnapshotAsync("hashed baseline", CancellationToken.None);
|
||||
Assert.Single(integritySnapshot.Items, item => item.ArtifactId == artifact.Id && item.Sha256 == content.Sha256);
|
||||
Assert.Contains(await store.ListSnapshotsAsync(CancellationToken.None), item => item.Id == integritySnapshot.Id);
|
||||
|
||||
var dolphinGameId = Guid.NewGuid();
|
||||
var dolphinArtifact = artifact with
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RelativePath = "roms/wii/Synthetic Fixture.rvz",
|
||||
Size = 2 * 1024 * 1024,
|
||||
Platform = "wii",
|
||||
Signature = "RVZ",
|
||||
ContentBlobId = null,
|
||||
Sha256 = null,
|
||||
Version = 1
|
||||
};
|
||||
await store.UpsertArtifactAsync(dolphinArtifact, null, [new Evidence("extension", "RVZ", "test", "1",
|
||||
Confidence.Deterministic, DateTimeOffset.UtcNow)], CancellationToken.None);
|
||||
// A parameterised command carries exactly one statement: PostgreSQL rejects several in the
|
||||
// extended query protocol, so the fixture rows are inserted separately.
|
||||
await using (var insertGame = dataSource.CreateCommand("""
|
||||
INSERT INTO games(id,title,version,data)
|
||||
VALUES($1,$2,1,jsonb_build_object('id',$1,'title',$2,'createdAt',now(),'version',1))
|
||||
"""))
|
||||
{
|
||||
insertGame.Parameters.AddWithValue(dolphinGameId);
|
||||
insertGame.Parameters.AddWithValue("Synthetic Dolphin Fixture");
|
||||
await insertGame.ExecuteNonQueryAsync();
|
||||
}
|
||||
await using (var link = dataSource.CreateCommand("""
|
||||
INSERT INTO game_artifact_links(game_id,artifact_id,source,created_at)
|
||||
VALUES($1,$2,'integration-fixture',now())
|
||||
"""))
|
||||
{
|
||||
link.Parameters.AddWithValue(dolphinGameId);
|
||||
link.Parameters.AddWithValue(dolphinArtifact.Id);
|
||||
await link.ExecuteNonQueryAsync();
|
||||
}
|
||||
var dolphinCandidate = Assert.Single(await store.ListDolphinLaunchCandidatesAsync(dolphinGameId, "wii",
|
||||
CancellationToken.None));
|
||||
Assert.Equal(dolphinArtifact.RelativePath, dolphinCandidate.RelativePath);
|
||||
Assert.True(dolphinCandidate.SourceReadOnly);
|
||||
Assert.Empty(await store.ListDolphinLaunchCandidatesAsync(dolphinGameId, "xbox", CancellationToken.None));
|
||||
|
||||
var titledArtifact = artifact with
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RelativePath = "roms/nes/Super.Game.64 (USA).nes",
|
||||
ContentBlobId = null,
|
||||
Sha256 = null,
|
||||
Version = 1
|
||||
};
|
||||
await store.UpsertArtifactAsync(titledArtifact, null, [evidence], CancellationToken.None);
|
||||
var legacyGameId = Guid.NewGuid();
|
||||
await using (var legacy = dataSource.CreateCommand("INSERT INTO games(id,title,version,data) VALUES($1,$2,1,jsonb_build_object('id',$1,'title',$2,'createdAt',now(),'version',1))"))
|
||||
{
|
||||
legacy.Parameters.AddWithValue(legacyGameId);
|
||||
legacy.Parameters.AddWithValue("0002 - Legacy Racer (USA) zip");
|
||||
await legacy.ExecuteNonQueryAsync();
|
||||
}
|
||||
await store.SynchronizeDiscoveredGamesAsync(library.Id, CancellationToken.None);
|
||||
var canonicalGame = Assert.Single((await store.SearchGamesAsync(new GameQuery { Search = "Super Game 64", Platform = "nes", Page = 1, PageSize = 25 }, CancellationToken.None)).Items);
|
||||
Assert.Equal("Super Game 64", canonicalGame.Title);
|
||||
Assert.Equal(GameOrigin.Scan, canonicalGame.Origin);
|
||||
await Assert.ThrowsAsync<ResourceConflictException>(() => store.DeleteGameAsync(canonicalGame.Id,
|
||||
canonicalGame.Version, "integration-test", CancellationToken.None));
|
||||
var browserCandidate = Assert.IsType<BrowserPlayCandidate>(
|
||||
await store.GetBrowserPlayCandidateAsync(canonicalGame.Id, CancellationToken.None));
|
||||
Assert.Equal(titledArtifact.Id, browserCandidate.ArtifactId);
|
||||
Assert.True(browserCandidate.SourceReadOnly);
|
||||
var playSession = new BrowserPlaySession(Guid.NewGuid(), canonicalGame.Id, titledArtifact.Id,
|
||||
PlaySessionState.Ready, "nes", "Ludarium EmulatorJS", "fceumm", "/player.html",
|
||||
new string('d', 64), DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddMinutes(30));
|
||||
await store.SaveBrowserPlaySessionAsync(playSession, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(1, await store.CountActiveBrowserPlaySessionsAsync(DateTimeOffset.UtcNow, CancellationToken.None));
|
||||
Assert.Equal(playSession.Id, (await store.GetBrowserPlaySessionAsync(playSession.Id, CancellationToken.None))?.Id);
|
||||
await store.EndBrowserPlaySessionAsync(playSession.Id, PlaySessionState.Cancelled, "integration-test",
|
||||
"cancelled", "Synthetic cancellation.", CancellationToken.None);
|
||||
Assert.Equal(PlaySessionState.Cancelled,
|
||||
(await store.GetBrowserPlaySessionAsync(playSession.Id, CancellationToken.None))?.State);
|
||||
await using (var playAudit = dataSource.CreateCommand(
|
||||
"SELECT string_agg(data::text,' ') FROM audit_events WHERE data->>'action' LIKE 'BrowserPlay%'"))
|
||||
{
|
||||
var redactedAudit = Assert.IsType<string>(await playAudit.ExecuteScalarAsync());
|
||||
Assert.DoesNotContain(titledArtifact.RelativePath, redactedAudit, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(playSession.TokenHash, redactedAudit, StringComparison.Ordinal);
|
||||
}
|
||||
Assert.Equal("Legacy Racer", Assert.Single((await store.SearchGamesAsync(new GameQuery { Search = "Legacy Racer", Page = 1, PageSize = 25 }, CancellationToken.None)).Items).Title);
|
||||
await store.UpsertArtifactAsync(titledArtifact with { Id = Guid.NewGuid(), RelativePath = "roms/nes/Super_Game_64 (USA).nes" }, null, [evidence], CancellationToken.None);
|
||||
await store.SynchronizeDiscoveredGamesAsync(library.Id, CancellationToken.None);
|
||||
Assert.Null(await store.GetBrowserPlayCandidateAsync(canonicalGame.Id, CancellationToken.None));
|
||||
Assert.Single(await store.ListReleasesAsync(canonicalGame.Id, CancellationToken.None));
|
||||
var libraryHealth = Assert.Single(await store.ListLibraryHealthAsync(CancellationToken.None), item => item.LibraryId == library.Id);
|
||||
Assert.True(libraryHealth.Artifacts >= 2);
|
||||
// The canonical NES game, the legacy racer and the linked Dolphin fixture all live in this library.
|
||||
Assert.Equal(3, libraryHealth.Games);
|
||||
Assert.Contains("nes", libraryHealth.DetectedPlatforms);
|
||||
var scanRelease = Assert.Single(await store.ListReleasesAsync(canonicalGame.Id, CancellationToken.None));
|
||||
Assert.Equal(ReleaseOrigin.Scan, scanRelease.Origin);
|
||||
await Assert.ThrowsAsync<ResourceConflictException>(() => store.UpdateReleaseAsync(scanRelease.Id,
|
||||
"Unsafe override", "nes", null, null, scanRelease.Version, "integration-test", CancellationToken.None));
|
||||
await Assert.ThrowsAsync<ResourceConflictException>(() => store.DeleteReleaseAsync(scanRelease.Id,
|
||||
scanRelease.Version, "integration-test", CancellationToken.None));
|
||||
var artwork = titledArtifact with
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RelativePath = "roms/nes/Super Game 64 (USA)/imgs/covers/cover1.jpg",
|
||||
MediaType = MediaType.SupportFile,
|
||||
State = ArtifactState.Ignored
|
||||
};
|
||||
await store.UpsertArtifactAsync(artwork, null, [], CancellationToken.None);
|
||||
await store.MarkArtifactIgnoredAsync(artwork.Id, 2, evidence, CancellationToken.None);
|
||||
Assert.Equal(artwork.Id, (await store.GetGameArtworkAsync(canonicalGame.Id, CancellationToken.None))?.Artifact.Id);
|
||||
|
||||
var game = await store.CreateGameAsync("Synthetic Game", "integration-test", CancellationToken.None);
|
||||
Assert.Equal(GameOrigin.Manual, game.Origin);
|
||||
Assert.Equal(game.Id, (await store.GetGameAsync(game.Id, CancellationToken.None))?.Id);
|
||||
Assert.Null(await store.GetGameAsync(Guid.NewGuid(), CancellationToken.None));
|
||||
var updated = await store.UpdateGameAsync(game.Id, "Synthetic Game Updated", game.Version, "integration-test", CancellationToken.None);
|
||||
var gamePage = await store.SearchGamesAsync(new GameQuery { Search = "Updated", Page = 1, PageSize = 25 }, CancellationToken.None);
|
||||
Assert.Equal(1, gamePage.Total);
|
||||
Assert.Equal(updated.Id, Assert.Single(gamePage.Items).Id);
|
||||
await Assert.ThrowsAsync<ConcurrencyException>(() =>
|
||||
store.UpdateGameAsync(game.Id, "Stale Update", game.Version, "integration-test", CancellationToken.None));
|
||||
var release = await store.CreateReleaseAsync(game.Id, "Synthetic Release", "nes", "world", null, "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReleasesAsync(game.Id, CancellationToken.None), item => item.Id == release.Id);
|
||||
Assert.Equal(ReleaseOrigin.Manual, release.Origin);
|
||||
await using (var linkPlayableGame = dataSource.CreateCommand("""
|
||||
INSERT INTO game_artifact_links(game_id,artifact_id,source,created_at)
|
||||
VALUES($1,$2,'integration-fixture',now()) ON CONFLICT DO NOTHING
|
||||
"""))
|
||||
{
|
||||
linkPlayableGame.Parameters.AddWithValue(game.Id);
|
||||
linkPlayableGame.Parameters.AddWithValue(artifact.Id);
|
||||
await linkPlayableGame.ExecuteNonQueryAsync();
|
||||
}
|
||||
var unsupportedGame = await store.CreateGameAsync("Synthetic Future Console Game", "integration-test", CancellationToken.None);
|
||||
await store.CreateReleaseAsync(unsupportedGame.Id, "Synthetic Future Console Release", "future-console", null, null,
|
||||
"integration-test", CancellationToken.None);
|
||||
var playability = await store.GetPlayabilitySummaryAsync(CancellationToken.None);
|
||||
Assert.Equal(playability.Supported, playability.Ready + playability.Blocked);
|
||||
Assert.True(playability.Ready >= 1);
|
||||
Assert.True(playability.Unsupported >= 1);
|
||||
Assert.Contains(playability.ByPlatform, item => item.Platform == "nes" && item.Ready >= 1);
|
||||
var updatedRelease = await store.UpdateReleaseAsync(release.Id, "Synthetic Release Revised", "nes", "world", "rev 2",
|
||||
release.Version, "integration-test", CancellationToken.None);
|
||||
Assert.Equal("rev 2", updatedRelease.Revision);
|
||||
await Assert.ThrowsAsync<ConcurrencyException>(() => store.UpdateReleaseAsync(release.Id, "Stale", "nes", null, null,
|
||||
release.Version, "integration-test", CancellationToken.None));
|
||||
var deletedRelease = await store.CreateReleaseAsync(game.Id, "Disposable Release", "nes", null, null,
|
||||
"integration-test", CancellationToken.None);
|
||||
await store.DeleteReleaseAsync(deletedRelease.Id, deletedRelease.Version, "integration-test", CancellationToken.None);
|
||||
Assert.DoesNotContain(await store.ListReleasesAsync(game.Id, CancellationToken.None), item => item.Id == deletedRelease.Id);
|
||||
var collection = await store.CreateCollectionAsync(new("Synthetic favorites", "A deterministic set",
|
||||
CollectionKind.Static, null, true), "integration-test", CancellationToken.None);
|
||||
await store.AddCollectionGameAsync(collection.Id, game.Id, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(game.Id, Assert.Single((await store.SearchCollectionGamesAsync(collection.Id, 1, 25, CancellationToken.None)).Items).Id);
|
||||
var tag = await store.AddGameTagAsync(game.Id, "Co-Op", "integration-test", CancellationToken.None);
|
||||
Assert.Equal("co-op", tag.NormalizedName);
|
||||
Assert.Single(await store.ListTagsAsync(CancellationToken.None), item => item.NormalizedName == "co-op" && item.GameCount == 1);
|
||||
var state = await store.SaveGameUserStateAsync(game.Id, new(true, GamePlayStatus.Playing, 9, 6, 42.5m,
|
||||
"Co-op campaign", 3, DateTimeOffset.UtcNow), 0, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(1, state.Version);
|
||||
var played = await store.RecordGamePlayedAsync(game.Id, "integration-test", CancellationToken.None);
|
||||
played = await store.RecordGamePlayedAsync(game.Id, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(5, played.PlayCount);
|
||||
Assert.Equal(GamePlayStatus.Playing, played.Status);
|
||||
Assert.NotNull(played.LastPlayedAt);
|
||||
var concurrentPlays = await Task.WhenAll(Enumerable.Range(0, 4).Select(_ =>
|
||||
store.RecordGamePlayedAsync(game.Id, "integration-test", CancellationToken.None)));
|
||||
Assert.Equal(9, (await store.GetGameUserStateAsync(game.Id, CancellationToken.None)).PlayCount);
|
||||
Assert.Equal(4, concurrentPlays.Select(item => item.PlayCount).Distinct().Count());
|
||||
var recent = Assert.Single(await store.ListRecentlyPlayedGamesAsync(6, CancellationToken.None));
|
||||
Assert.Equal(game.Id, recent.Game.Id);
|
||||
Assert.Equal(9, recent.State.PlayCount);
|
||||
await Assert.ThrowsAsync<ConcurrencyException>(() => store.SaveGameUserStateAsync(game.Id,
|
||||
new(false, GamePlayStatus.Backlog, null, null, null, null, 0, null), 0, "integration-test", CancellationToken.None));
|
||||
var smart = await store.CreateCollectionAsync(new("Currently playing", null, CollectionKind.Smart,
|
||||
new CollectionRule(Tags: ["co-op"], Status: GamePlayStatus.Playing, Favorite: true), false),
|
||||
"integration-test", CancellationToken.None);
|
||||
Assert.Contains((await store.SearchCollectionGamesAsync(smart.Id, 1, 25, CancellationToken.None)).Items, item => item.Id == game.Id);
|
||||
var relation = await store.AddGameRelationAsync(game.Id, new(GameRelationKind.Extra, "Soundtrack", null, null),
|
||||
"integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListGameRelationsAsync(game.Id, CancellationToken.None), item => item.Id == relation.Id);
|
||||
var media = await store.AddExternalGameMediaAsync(game.Id, new(GameMediaKind.Manual, "Online manual",
|
||||
"https://example.test/manual.pdf", "Synthetic", "manual-1"), "integration-test", CancellationToken.None);
|
||||
Assert.True(media.Selected);
|
||||
var achievementProgress = new GameAchievementProgress(game.Id, "Synthetic", "game-1", 1, 1,
|
||||
DateTimeOffset.UtcNow, [new(Guid.NewGuid(), "achievement-1", "First step", null, 5, true, DateTimeOffset.UtcNow, null)]);
|
||||
await store.UpsertAchievementProgressAsync(achievementProgress, CancellationToken.None);
|
||||
Assert.Equal(1, (await store.GetAchievementProgressAsync(game.Id, CancellationToken.None))?.Earned);
|
||||
var dataEntryId = Guid.NewGuid();
|
||||
var firstDataRevision = new GameDataRevision(Guid.NewGuid(), dataEntryId, 0, "slot-1.sav",
|
||||
"application/octet-stream", 16, new string('a', 64),
|
||||
$"{game.Id:N}/{dataEntryId:N}/{Guid.NewGuid():N}.bin", "ManualUpload", "integration-test",
|
||||
DateTimeOffset.UtcNow);
|
||||
firstDataRevision = firstDataRevision with
|
||||
{
|
||||
Location = $"{game.Id:N}/{dataEntryId:N}/{firstDataRevision.Id:N}.bin"
|
||||
};
|
||||
var dataInput = new GameDataEntryInput(GameDataKind.Save, "Main campaign", "mGBA", "Steam Deck",
|
||||
"Before the final dungeon");
|
||||
var concurrentDataWrites = await Task.WhenAll(
|
||||
store.SaveGameDataRevisionAsync(game.Id, dataEntryId, dataInput, 0, firstDataRevision,
|
||||
"integration-test", CancellationToken.None),
|
||||
store.SaveGameDataRevisionAsync(game.Id, dataEntryId, dataInput, 0, firstDataRevision,
|
||||
"integration-test", CancellationToken.None));
|
||||
var dataItem = concurrentDataWrites[0];
|
||||
Assert.Equal(1, dataItem.Entry.Version);
|
||||
Assert.Equal(dataItem.Entry.Id, concurrentDataWrites[1].Entry.Id);
|
||||
var secondDataRevision = firstDataRevision with
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Length = 24,
|
||||
Sha256 = new string('b', 64),
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddSeconds(1)
|
||||
};
|
||||
secondDataRevision = secondDataRevision with
|
||||
{
|
||||
Location = $"{game.Id:N}/{dataEntryId:N}/{secondDataRevision.Id:N}.bin"
|
||||
};
|
||||
dataItem = await store.SaveGameDataRevisionAsync(game.Id, dataEntryId,
|
||||
new(GameDataKind.Save, "Main campaign", "mGBA", "Steam Deck", "After the final dungeon"),
|
||||
dataItem.Entry.Version, secondDataRevision, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(2, dataItem.Entry.RevisionCount);
|
||||
var reboundDataRevision = secondDataRevision with
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
SourceId = "artifact-version-2",
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddSeconds(2)
|
||||
};
|
||||
reboundDataRevision = reboundDataRevision with
|
||||
{
|
||||
Location = $"{game.Id:N}/{dataEntryId:N}/{reboundDataRevision.Id:N}.bin"
|
||||
};
|
||||
dataItem = await store.SaveGameDataRevisionAsync(game.Id, dataEntryId, dataInput,
|
||||
dataItem.Entry.Version, reboundDataRevision, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(3, dataItem.Entry.RevisionCount);
|
||||
Assert.Equal("artifact-version-2", dataItem.CurrentRevision.SourceId);
|
||||
Assert.Equal(3, (await store.ListGameDataRevisionsAsync(dataEntryId, 1, 25, CancellationToken.None)).Total);
|
||||
var revisionLocations = await store.ListGameDataRevisionLocationsAsync(CancellationToken.None);
|
||||
Assert.Contains(firstDataRevision.Location, revisionLocations);
|
||||
Assert.Contains(secondDataRevision.Location, revisionLocations);
|
||||
dataItem = await store.UpdateGameDataAsync(game.Id, dataEntryId,
|
||||
new("Main campaign", "mGBA", "Living room handheld", "Verified post-game save"),
|
||||
dataItem.Entry.Version, "integration-test", CancellationToken.None);
|
||||
Assert.Equal("Living room handheld", dataItem.Entry.Device);
|
||||
await Assert.ThrowsAsync<ConcurrencyException>(() => store.UpdateGameDataAsync(game.Id, dataEntryId,
|
||||
new("Stale", null, null, null), 1, "integration-test", CancellationToken.None));
|
||||
var dataSummary = await store.GetGameDataSummaryAsync(game.Id, CancellationToken.None);
|
||||
Assert.Equal(1, dataSummary.Saves);
|
||||
Assert.Equal(3, dataSummary.Revisions);
|
||||
Assert.Equal(64, dataSummary.Bytes);
|
||||
var customPlatform = await store.CreatePlatformDefinitionAsync(new("synthetic-console", "Synthetic Console",
|
||||
"Synthetic", ["synth"], true), "integration-test", CancellationToken.None);
|
||||
var disabledPlatform = await store.UpdatePlatformDefinitionAsync(customPlatform.Id,
|
||||
new(customPlatform.Id, customPlatform.Name, customPlatform.Category, customPlatform.Aliases, false),
|
||||
customPlatform.Version, "integration-test", CancellationToken.None);
|
||||
Assert.False(disabledPlatform.Enabled);
|
||||
Assert.Contains((await store.SearchGamesAsync(new GameQuery { Platform = "nes", Page = 1, PageSize = 25 }, CancellationToken.None)).Items, item => item.Id == game.Id);
|
||||
Assert.Empty((await store.SearchGamesAsync(new GameQuery { Platform = "ps5", Page = 1, PageSize = 25 }, CancellationToken.None)).Items);
|
||||
var input = new WishlistInput(game.Id, game.Title, "nes", WishlistPriority.High, "Find a boxed copy", WishlistStatus.WaitingForSale, 25, 20, "EUR", "Synthetic Store", "https://example.test/game", "Boxed", "EU");
|
||||
var wish = await store.CreateWishlistItemAsync(input, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(wish.Id, Assert.Single((await store.SearchWishlistAsync("boxed", "nes", WishlistPriority.High, WishlistStatus.WaitingForSale, "price", 1, 25, CancellationToken.None)).Items).Id);
|
||||
Assert.Equal(1, (await store.GetWishlistSummaryAsync(CancellationToken.None)).PriceDrops);
|
||||
await Assert.ThrowsAsync<ResourceConflictException>(() => store.CreateWishlistItemAsync(input, "integration-test", CancellationToken.None));
|
||||
var updatedInput = input with { Priority = WishlistPriority.Low, Notes = "Wait for a sale", CurrentPrice = 30 };
|
||||
var updatedWish = await store.UpdateWishlistItemAsync(wish.Id, updatedInput, wish.Version, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(WishlistPriority.Low, updatedWish.Priority);
|
||||
await Assert.ThrowsAsync<ConcurrencyException>(() => store.UpdateWishlistItemAsync(wish.Id, input, wish.Version, "integration-test", CancellationToken.None));
|
||||
await store.DeleteWishlistItemAsync(updatedWish.Id, updatedWish.Version, "integration-test", CancellationToken.None);
|
||||
var futureWish = await store.CreateWishlistItemAsync(new(null, "Unique Future Game", "ps5", WishlistPriority.Normal, null), "integration-test", CancellationToken.None);
|
||||
var uniqueFutureGame = await store.CreateGameAsync("Unique Future Game", "integration-test", CancellationToken.None);
|
||||
Assert.Equal(1, await store.ReconcileWishlistAsync("integration-test", CancellationToken.None));
|
||||
var reconciled = Assert.Single((await store.SearchWishlistAsync("Unique Future Game", null, null, WishlistStatus.Acquired, null, 1, 25, CancellationToken.None)).Items);
|
||||
Assert.Equal(uniqueFutureGame.Id, reconciled.GameId);
|
||||
await store.DeleteWishlistItemAsync(reconciled.Id, reconciled.Version, "integration-test", CancellationToken.None);
|
||||
var ambiguousWish = await store.CreateWishlistItemAsync(new(null, "Ambiguous Future Game", null, WishlistPriority.Normal, null), "integration-test", CancellationToken.None);
|
||||
await store.CreateGameAsync("Ambiguous Future Game", "integration-test", CancellationToken.None);
|
||||
await store.CreateGameAsync("Ambiguous Future Game", "integration-test", CancellationToken.None);
|
||||
Assert.Equal(0, await store.ReconcileWishlistAsync("integration-test", CancellationToken.None));
|
||||
var stillUnlinked = Assert.Single((await store.SearchWishlistAsync("Ambiguous Future Game", null, null, WishlistStatus.Interested, null, 1, 25, CancellationToken.None)).Items);
|
||||
Assert.Null(stillUnlinked.GameId);
|
||||
await store.DeleteWishlistItemAsync(stillUnlinked.Id, stillUnlinked.Version, "integration-test", CancellationToken.None);
|
||||
Assert.Empty((await store.SearchWishlistAsync(null, null, null, null, null, 1, 25, CancellationToken.None)).Items);
|
||||
var visiblePage = await store.SearchGamesAsync(new GameQuery { Page = 1, PageSize = 200 }, CancellationToken.None);
|
||||
var dashboard = await store.GetDashboardSummaryAsync(CancellationToken.None);
|
||||
Assert.Equal(visiblePage.Total, dashboard.Games);
|
||||
var dormant = LibraryRoot.Create("Dormant", Path.GetFullPath(Path.Combine(Path.GetTempPath(),
|
||||
"ludarium-dormant-library")), LibraryKind.Mixed) with
|
||||
{ Enabled = false, IsReadOnly = false, IsAvailable = false, HasEntries = false };
|
||||
await store.UpsertLibraryAsync(dormant, CancellationToken.None);
|
||||
Assert.Equal(dashboard.RootsRequiringAttention,
|
||||
(await store.GetDashboardSummaryAsync(CancellationToken.None)).RootsRequiringAttention);
|
||||
var nesPlatform = Assert.Single(await store.ListPlatformsAsync(CancellationToken.None), item => item.Platform == "nes");
|
||||
Assert.Equal((await store.SearchGamesAsync(new GameQuery { Platform = "nes", Page = 1, PageSize = 200 }, CancellationToken.None)).Total, nesPlatform.Games);
|
||||
|
||||
var hiddenSupportGame = await store.CreateGameAsync("Steam", "integration-test", CancellationToken.None);
|
||||
Assert.Null(await store.GetGameAsync(hiddenSupportGame.Id, CancellationToken.None));
|
||||
var supportAwareDashboard = await store.GetDashboardSummaryAsync(CancellationToken.None);
|
||||
Assert.Equal(visiblePage.Total, supportAwareDashboard.Games);
|
||||
Assert.Equal(1, supportAwareDashboard.ExcludedSupportGames);
|
||||
var defaults = await store.GetOperationalSettingsAsync(CancellationToken.None);
|
||||
var settings = await store.SaveOperationalSettingsAsync(defaults with { DefaultPageSize = 100 }, defaults.Version, CancellationToken.None);
|
||||
Assert.Equal(100, settings.DefaultPageSize);
|
||||
await Assert.ThrowsAsync<ConcurrencyException>(() => store.SaveOperationalSettingsAsync(defaults, defaults.Version, CancellationToken.None));
|
||||
var artworkRecord = new GameArtwork(Guid.NewGuid(), game.Id, ArtworkRole.VerifiedBoxFront,
|
||||
ArtworkVerificationStatus.AutomaticallyVerified, "Synthetic", "fixture", "art-1", "exact-test",
|
||||
Confidence.Deterministic, 600, 900, 0.6667m, 1234, new string('c', 64), 100m, DateTimeOffset.UtcNow);
|
||||
await store.UpsertGameArtworkAsync(artworkRecord, CancellationToken.None);
|
||||
Assert.Equal(artworkRecord.Id, (await store.GetSelectedGameArtworkAsync(game.Id, CancellationToken.None))?.Id);
|
||||
var alternateArtwork = artworkRecord with { Id = Guid.NewGuid(), Source = "Alternate", QualityScore = 82m, Selected = false };
|
||||
await store.UpsertGameArtworkAsync(alternateArtwork, CancellationToken.None);
|
||||
Assert.Equal(2, (await store.ListGameArtworkAsync(game.Id, CancellationToken.None)).Count);
|
||||
await store.SelectGameArtworkAsync(game.Id, alternateArtwork.Id, CancellationToken.None);
|
||||
Assert.Equal(alternateArtwork.Id, (await store.GetSelectedGameArtworkAsync(game.Id, CancellationToken.None))?.Id);
|
||||
await store.SelectGameArtworkAsync(game.Id, artworkRecord.Id, CancellationToken.None);
|
||||
Assert.Equal(artworkRecord.Id, (await store.GetSelectedGameArtworkAsync(game.Id, CancellationToken.None))?.Id);
|
||||
Assert.Single(await store.ListGameArtworkAsync(game.Id, CancellationToken.None), item => item.Selected);
|
||||
var ps5Artwork = artworkRecord with { Id = Guid.NewGuid(), Platform = "ps5" };
|
||||
var windowsArtwork = artworkRecord with { Id = Guid.NewGuid(), Platform = "windows" };
|
||||
await store.UpsertGameArtworkAsync(ps5Artwork, CancellationToken.None);
|
||||
await store.UpsertGameArtworkAsync(windowsArtwork, CancellationToken.None);
|
||||
Assert.Equal(ps5Artwork.Id,
|
||||
(await store.GetSelectedGameArtworkAsync(game.Id, CancellationToken.None, "ps5"))?.Id);
|
||||
Assert.Equal(windowsArtwork.Id,
|
||||
(await store.GetSelectedGameArtworkAsync(game.Id, CancellationToken.None, "windows"))?.Id);
|
||||
Assert.Equal(3, (await store.ListGameArtworkAsync(game.Id, CancellationToken.None)).Count(item => item.Selected));
|
||||
await Assert.ThrowsAsync<KeyNotFoundException>(() => store.SelectGameArtworkAsync(game.Id, Guid.NewGuid(), CancellationToken.None));
|
||||
Assert.DoesNotContain(await store.ListArtworkNeedingReviewAsync(CancellationToken.None), item => item.Id == artworkRecord.Id);
|
||||
|
||||
var disposableGame = await store.CreateGameAsync("Disposable Manual Game", "integration-test", CancellationToken.None);
|
||||
await Assert.ThrowsAsync<ConcurrencyException>(() => store.DeleteGameAsync(disposableGame.Id,
|
||||
disposableGame.Version - 1, "integration-test", CancellationToken.None));
|
||||
await store.CreateReleaseAsync(disposableGame.Id, "Disposable Version", "nes", null, null,
|
||||
"integration-test", CancellationToken.None);
|
||||
await store.AddGameTagAsync(disposableGame.Id, "Disposable", "integration-test", CancellationToken.None);
|
||||
var retainedWish = await store.CreateWishlistItemAsync(new(disposableGame.Id, disposableGame.Title, "nes",
|
||||
WishlistPriority.Normal, "Retain this intent"), "integration-test", CancellationToken.None);
|
||||
var deletion = await store.DeleteGameAsync(disposableGame.Id, disposableGame.Version,
|
||||
"integration-test", CancellationToken.None);
|
||||
Assert.Equal(disposableGame.Id, deletion.Game.Id);
|
||||
Assert.DoesNotContain(await store.ListGamesAsync(CancellationToken.None), item => item.Id == disposableGame.Id);
|
||||
Assert.Empty(await store.ListReleasesAsync(disposableGame.Id, CancellationToken.None));
|
||||
var standaloneWish = Assert.Single((await store.SearchWishlistAsync("Disposable Manual Game", null, null,
|
||||
null, null, 1, 25, CancellationToken.None)).Items);
|
||||
Assert.Equal(retainedWish.Id, standaloneWish.Id);
|
||||
Assert.Null(standaloneWish.GameId);
|
||||
Assert.Equal(retainedWish.Version + 1, standaloneWish.Version);
|
||||
await store.DeleteWishlistItemAsync(standaloneWish.Id, standaloneWish.Version, "integration-test", CancellationToken.None);
|
||||
|
||||
var queuedJob = await store.EnqueueJobAsync("Synthetic", "{}", "synthetic-job", CancellationToken.None);
|
||||
Assert.Equal(queuedJob.Id, (await store.EnqueueJobAsync("Synthetic", "{}", "synthetic-job", CancellationToken.None)).Id);
|
||||
var leasedJob = Assert.IsType<BackgroundJob>(await store.LeaseJobAsync("integration-worker", TimeSpan.FromMinutes(1), CancellationToken.None));
|
||||
Assert.Equal(JobState.Leased, leasedJob.State);
|
||||
await store.UpdateJobAsync(leasedJob with { State = JobState.Completed, LeaseOwner = null, LeaseUntil = null }, CancellationToken.None);
|
||||
Assert.Contains(await store.ListJobsAsync(CancellationToken.None), item => item.Id == queuedJob.Id && item.State == JobState.Completed);
|
||||
var openReviewsBeforeLifecycle = (await store.GetDashboardSummaryAsync(CancellationToken.None)).OpenReviews;
|
||||
var review = new ReviewItem(Guid.NewGuid(), "Synthetic ambiguity", Severity.Notice, ReviewState.Open,
|
||||
"{}", DateTimeOffset.UtcNow);
|
||||
await store.AddReviewAsync(review, CancellationToken.None);
|
||||
Assert.Equal(openReviewsBeforeLifecycle + 1,
|
||||
(await store.GetDashboardSummaryAsync(CancellationToken.None)).OpenReviews);
|
||||
var reviewGroup = Assert.Single(await store.ListReviewGroupsAsync(ReviewState.Open, CancellationToken.None),
|
||||
group => group.ReviewIds.Contains(review.Id));
|
||||
var filteredReviewGroups = await store.SearchReviewGroupsAsync(new ReviewGroupQuery
|
||||
{
|
||||
State = ReviewState.Open,
|
||||
Search = "Synthetic ambiguity",
|
||||
Severity = Severity.Notice,
|
||||
Order = ReviewGroupOrder.Newest,
|
||||
Page = 1,
|
||||
PageSize = 1
|
||||
}, CancellationToken.None);
|
||||
Assert.Contains(filteredReviewGroups.Items, group => group.ReviewIds.Contains(review.Id));
|
||||
Assert.True(filteredReviewGroups.Total >= 1);
|
||||
Assert.Equal(0, (await store.SearchReviewGroupsAsync(new ReviewGroupQuery
|
||||
{
|
||||
State = ReviewState.Open,
|
||||
Search = "no-such-review-group",
|
||||
Page = 1,
|
||||
PageSize = 50
|
||||
}, CancellationToken.None)).Total);
|
||||
var operation = await store.ResolveReviewGroupAsync(reviewGroup.ReviewIds, (int)reviewGroup.Count,
|
||||
"accepted exact test group", "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id && item.State == ReviewState.Resolved);
|
||||
await store.UndoReviewOperationAsync(operation.Id, "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id && item.State == ReviewState.Open);
|
||||
var deferredOperation = await store.DeferReviewGroupAsync(reviewGroup.ReviewIds, (int)reviewGroup.Count,
|
||||
"deferred exact test group", "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id && item.State == ReviewState.Deferred);
|
||||
Assert.Equal(openReviewsBeforeLifecycle,
|
||||
(await store.GetDashboardSummaryAsync(CancellationToken.None)).OpenReviews);
|
||||
var deferredGroup = Assert.Single(await store.ListReviewGroupsAsync(ReviewState.Deferred, CancellationToken.None),
|
||||
group => group.ReviewIds.Contains(review.Id));
|
||||
var reopenedOperation = await store.ReopenReviewGroupAsync(deferredGroup.ReviewIds, (int)deferredGroup.Count, ReviewState.Deferred,
|
||||
"reopened exact test group", "integration-test", CancellationToken.None);
|
||||
var reopenedReview = Assert.Single(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id);
|
||||
Assert.Equal(ReviewState.Open, reopenedReview.State);
|
||||
Assert.Null(reopenedReview.Resolution);
|
||||
Assert.Equal(openReviewsBeforeLifecycle + 1,
|
||||
(await store.GetDashboardSummaryAsync(CancellationToken.None)).OpenReviews);
|
||||
var resolvedOperation = await store.ResolveReviewGroupAsync(reopenedOperation.Before.Select(item => item.Id).ToArray(),
|
||||
reopenedOperation.Before.Count, "resolved exact test group", "integration-test", CancellationToken.None);
|
||||
var resolvedGroup = Assert.Single(await store.ListReviewGroupsAsync(ReviewState.Resolved, CancellationToken.None),
|
||||
group => group.ReviewIds.Contains(review.Id));
|
||||
Assert.Equal("resolved exact test group", resolvedGroup.Resolution);
|
||||
Assert.NotNull(resolvedGroup.ResolvedAt);
|
||||
var reopenedResolvedOperation = await store.ReopenReviewGroupAsync(resolvedGroup.ReviewIds, (int)resolvedGroup.Count, ReviewState.Resolved,
|
||||
"reopened resolved test group", "integration-test", CancellationToken.None);
|
||||
var reopenedResolvedReview = Assert.Single(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id);
|
||||
Assert.Equal(ReviewState.Open, reopenedResolvedReview.State);
|
||||
Assert.Null(reopenedResolvedReview.Resolution);
|
||||
Assert.Null(reopenedResolvedReview.ResolvedAt);
|
||||
await store.UndoReviewOperationAsync(reopenedResolvedOperation.Id, "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id && item.State == ReviewState.Resolved);
|
||||
await store.UndoReviewOperationAsync(resolvedOperation.Id, "integration-test", CancellationToken.None);
|
||||
await store.UndoReviewOperationAsync(reopenedOperation.Id, "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id && item.State == ReviewState.Deferred);
|
||||
await store.UndoReviewOperationAsync(deferredOperation.Id, "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id && item.State == ReviewState.Open);
|
||||
var artifactReview = new ReviewItem(Guid.NewGuid(), "Unknown content", Severity.Notice, ReviewState.Open,
|
||||
System.Text.Json.JsonSerializer.Serialize(new { artifact.Id, artifact.RelativePath }), DateTimeOffset.UtcNow);
|
||||
await store.AddReviewAsync(artifactReview, CancellationToken.None);
|
||||
var artifactGroup = Assert.Single(await store.ListReviewGroupsAsync(ReviewState.Open, CancellationToken.None),
|
||||
group => group.ReviewIds.Contains(artifactReview.Id));
|
||||
var supportOperation = await store.ApplyReviewGroupActionAsync(artifactGroup.ReviewIds, (int)artifactGroup.Count,
|
||||
ReviewBulkAction.ClassifySupportContent, null, null, "integration-test", CancellationToken.None);
|
||||
var classifiedSupport = Assert.IsType<ArtifactDetails>(await store.GetArtifactDetailsAsync(artifact.Id, CancellationToken.None));
|
||||
Assert.Equal(ArtifactState.Ignored, classifiedSupport.Artifact.State);
|
||||
Assert.Equal(MediaType.SupportFile, classifiedSupport.Artifact.MediaType);
|
||||
Assert.Contains(classifiedSupport.Evidence, item => item.Kind == "review.bulk-action" && item.Value == "support-content");
|
||||
await store.UndoReviewOperationAsync(supportOperation.Id, "integration-test", CancellationToken.None);
|
||||
var restoredSupportArtifact = Assert.IsType<ArtifactDetails>(await store.GetArtifactDetailsAsync(artifact.Id, CancellationToken.None));
|
||||
Assert.Equal(ArtifactState.Present, restoredSupportArtifact.Artifact.State);
|
||||
Assert.DoesNotContain(restoredSupportArtifact.Evidence, item => item.Kind == "review.bulk-action" && item.Value == "support-content");
|
||||
var platformOperation = await store.ApplyReviewGroupActionAsync(artifactGroup.ReviewIds, (int)artifactGroup.Count,
|
||||
ReviewBulkAction.AcceptPlatform, "ps5", null, "integration-test", CancellationToken.None);
|
||||
Assert.Equal("ps5", (await store.GetArtifactDetailsAsync(artifact.Id, CancellationToken.None))!.Artifact.Platform);
|
||||
await store.UndoReviewOperationAsync(platformOperation.Id, "integration-test", CancellationToken.None);
|
||||
var restoredPlatformArtifact = Assert.IsType<ArtifactDetails>(await store.GetArtifactDetailsAsync(artifact.Id, CancellationToken.None));
|
||||
Assert.Equal("nes", restoredPlatformArtifact.Artifact.Platform);
|
||||
Assert.DoesNotContain(restoredPlatformArtifact.Evidence, item => item.Kind == "review.bulk-action" && item.Value == "platform:ps5");
|
||||
var exclusionOperation = await store.ApplyReviewGroupActionAsync(artifactGroup.ReviewIds, (int)artifactGroup.Count,
|
||||
ReviewBulkAction.ExcludeFolderPattern, null, artifactGroup.PathPattern, "integration-test", CancellationToken.None);
|
||||
Assert.Contains("roms/nes/**", (await store.GetLibraryAsync(library.Id, CancellationToken.None))!.Exclusions!);
|
||||
await store.UndoReviewOperationAsync(exclusionOperation.Id, "integration-test", CancellationToken.None);
|
||||
Assert.Empty((await store.GetLibraryAsync(library.Id, CancellationToken.None))!.Exclusions ?? []);
|
||||
var resolution = await store.ResolveReviewAsync(review.Id, "accepted", "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id && item.State == ReviewState.Resolved);
|
||||
await store.UndoResolutionAsync(resolution.Id, "integration-test", CancellationToken.None);
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == review.Id && item.State == ReviewState.Open);
|
||||
|
||||
var staleUndoReview = new ReviewItem(Guid.NewGuid(), "Synthetic stale undo", Severity.Notice, ReviewState.Open,
|
||||
"{}", DateTimeOffset.UtcNow);
|
||||
await store.AddReviewAsync(staleUndoReview, CancellationToken.None);
|
||||
var staleUndoGroup = Assert.Single(await store.ListReviewGroupsAsync(ReviewState.Open, CancellationToken.None),
|
||||
group => group.ReviewIds.Contains(staleUndoReview.Id));
|
||||
var staleUndoOperation = await store.DeferReviewGroupAsync(staleUndoGroup.ReviewIds, (int)staleUndoGroup.Count,
|
||||
"deferred before a later action", "integration-test", CancellationToken.None);
|
||||
await store.SetReviewStateAsync(staleUndoReview.Id, ReviewState.Open, "integration-test", CancellationToken.None);
|
||||
await Assert.ThrowsAsync<ConcurrencyException>(() => store.UndoReviewOperationAsync(staleUndoOperation.Id,
|
||||
"integration-test", CancellationToken.None));
|
||||
Assert.Contains(await store.ListReviewsAsync(CancellationToken.None), item => item.Id == staleUndoReview.Id && item.State == ReviewState.Open);
|
||||
|
||||
var mixedReason = $"Mixed resolution {Guid.NewGuid():N}";
|
||||
await store.AddReviewAsync(new ReviewItem(Guid.NewGuid(), mixedReason, Severity.Notice, ReviewState.Resolved,
|
||||
"{}", DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, "one recorded outcome"), CancellationToken.None);
|
||||
await store.AddReviewAsync(new ReviewItem(Guid.NewGuid(), mixedReason, Severity.Notice, ReviewState.Resolved,
|
||||
"{}", DateTimeOffset.UtcNow), CancellationToken.None);
|
||||
var mixedGroup = Assert.Single(await store.ListReviewGroupsAsync(ReviewState.Resolved, CancellationToken.None),
|
||||
group => group.Reason == mixedReason);
|
||||
Assert.Null(mixedGroup.Resolution);
|
||||
Assert.Null(mixedGroup.ResolvedAt);
|
||||
|
||||
var catalogSource = new CatalogSource(Guid.NewGuid(), "Synthetic CRC catalog", "logiqx", "1",
|
||||
null, "Ready", DateTimeOffset.UtcNow, 1);
|
||||
var catalogEntry = new CatalogEntry(Guid.NewGuid(), catalogSource.Id, "Synthetic", artifact.RelativePath,
|
||||
artifact.Size, null, null, content.Crc32, "synthetic-crc");
|
||||
await store.UpsertCatalogSourceAsync(catalogSource, CancellationToken.None);
|
||||
await store.UpsertCatalogEntriesAsync([catalogEntry], CancellationToken.None);
|
||||
Assert.Equal(1, await store.MatchCatalogAsync(catalogSource.Id, CancellationToken.None));
|
||||
var catalogReviewGroup = Assert.Single(await store.ListReviewGroupsAsync(ReviewState.Open, CancellationToken.None),
|
||||
group => group.Reason == "Catalog ambiguity" && group.ReviewIds.Count == 1);
|
||||
await store.DeferReviewGroupAsync(catalogReviewGroup.ReviewIds, (int)catalogReviewGroup.Count,
|
||||
"deferred catalog ambiguity", "integration-test", CancellationToken.None);
|
||||
Assert.Equal(1, await store.MatchCatalogAsync(catalogSource.Id, CancellationToken.None));
|
||||
var deferredCatalogReview = Assert.Single(await store.ListReviewsAsync(CancellationToken.None),
|
||||
item => item.Reason == "Catalog ambiguity" && item.State == ReviewState.Deferred);
|
||||
Assert.Equal("deferred catalog ambiguity", deferredCatalogReview.Resolution);
|
||||
|
||||
await using var restartedDataSource = NpgsqlDataSource.Create(postgres.GetConnectionString());
|
||||
var restartedStore = new PostgresStore(restartedDataSource);
|
||||
await restartedStore.InitializeAsync(CancellationToken.None);
|
||||
Assert.Contains(await restartedStore.ListLibrariesAsync(CancellationToken.None), item => item.Id == library.Id);
|
||||
Assert.Contains(await restartedStore.ListGamesAsync(CancellationToken.None), item => item.Id == updated.Id && item.Title == updated.Title);
|
||||
Assert.Equal(2, (await restartedStore.ListClaimsAsync("Game", game.Id, CancellationToken.None)).Count);
|
||||
Assert.Equal(100, (await restartedStore.GetOperationalSettingsAsync(CancellationToken.None)).DefaultPageSize);
|
||||
Assert.True((await restartedStore.GetGameUserStateAsync(game.Id, CancellationToken.None)).Favorite);
|
||||
Assert.Contains(await restartedStore.ListCollectionsAsync(CancellationToken.None), item => item.Id == collection.Id && item.GameCount == 1);
|
||||
Assert.Contains(await restartedStore.ListPlatformDefinitionsAsync(CancellationToken.None), item => item.Id == customPlatform.Id && !item.Enabled);
|
||||
var restartedDataItem = Assert.Single(await restartedStore.ListGameDataAsync(game.Id, CancellationToken.None));
|
||||
Assert.Equal(dataItem.Entry.Version, restartedDataItem.Entry.Version);
|
||||
var deletedDataRevisions = await restartedStore.DeleteGameDataAsync(game.Id, dataEntryId,
|
||||
restartedDataItem.Entry.Version, "integration-test", CancellationToken.None);
|
||||
Assert.Equal(3, deletedDataRevisions.Count);
|
||||
Assert.Empty(await restartedStore.ListGameDataAsync(game.Id, CancellationToken.None));
|
||||
|
||||
await using (var scale = dataSource.CreateCommand("""
|
||||
INSERT INTO artifacts(id,library_id,relative_path,size,data,updated_at)
|
||||
SELECT gen_random_uuid(), $1,
|
||||
CASE WHEN value % 1000 = 0 THEN 'roms/nes/needle-' ELSE 'roms/nes/synthetic-' END || lpad(value::text, 6, '0') || '.nes',
|
||||
32768, '{}'::jsonb, now()
|
||||
FROM generate_series(1,250000) AS value
|
||||
"""))
|
||||
{
|
||||
scale.CommandTimeout = 120;
|
||||
scale.Parameters.AddWithValue(library.Id);
|
||||
Assert.Equal(250_000, await scale.ExecuteNonQueryAsync());
|
||||
}
|
||||
await using (var analyze = dataSource.CreateCommand("EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM artifacts WHERE relative_path ILIKE '%needle%' ORDER BY relative_path LIMIT 50"))
|
||||
await using (var reader = await ExecuteScaleQueryAsync(analyze))
|
||||
{
|
||||
var plan = new List<string>();
|
||||
while (await reader.ReadAsync()) plan.Add(reader.GetString(0));
|
||||
Assert.Contains(plan, line => line.Contains("ix_artifacts_path_trgm", StringComparison.Ordinal));
|
||||
Assert.Contains(plan, line => line.Contains("Execution Time", StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<NpgsqlDataReader> ExecuteScaleQueryAsync(NpgsqlCommand command)
|
||||
{
|
||||
command.CommandTimeout = 120;
|
||||
return command.ExecuteReaderAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user