Files
Ludarium release export df869819ce
Public source validation / source (push) Successful in 2m16s
Publish Ludarium source
2026-09-03 02:08:58 +02:00

122 lines
7.6 KiB
C#

using Ludarium.Application;
using Ludarium.Domain;
using Ludarium.Infrastructure;
using Npgsql;
using Testcontainers.PostgreSql;
namespace Ludarium.IntegrationTests;
/// <summary>
/// The library browser's query, executed by the real database.
/// </summary>
/// <remarks>
/// The library used to sort the forty-eight games already on screen, which reordered a page instead
/// of the collection, and it could not filter on the favourites, status, ratings or tags it lets an
/// operator set. Both are properties of the query, so both are proven here against PostgreSQL.
/// </remarks>
public sealed class GameLibraryQueryTests
{
[Fact]
[Trait("Category", "Container")]
public async Task LibraryFiltersAndOrderingAreAnsweredByTheDatabase()
{
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_query_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);
var library = LibraryRoot.Create("Query fixture",
Path.GetFullPath(Path.Combine(Path.GetTempPath(), "ludarium-query-library")), LibraryKind.Rom) with
{ IsReadOnly = true, IsAvailable = true };
await store.UpsertLibraryAsync(library, CancellationToken.None);
var alpha = await store.CreateGameAsync("Alpha Adventure", "integration-test", CancellationToken.None);
var bravo = await store.CreateGameAsync("Bravo Blast", "integration-test", CancellationToken.None);
var charlie = await store.CreateGameAsync("Charlie Chase", "integration-test", CancellationToken.None);
await store.SaveGameUserStateAsync(alpha.Id,
new(true, GamePlayStatus.Playing, 5, null, 40m, null, 3, DateTimeOffset.UtcNow.AddDays(-1)),
0, "integration-test", CancellationToken.None);
await store.SaveGameUserStateAsync(bravo.Id,
new(false, GamePlayStatus.Completed, 3, null, 100m, null, 9, DateTimeOffset.UtcNow),
0, "integration-test", CancellationToken.None);
await store.AddGameTagAsync(charlie.Id, "Co-op", "integration-test", CancellationToken.None);
// --- filters ---------------------------------------------------------------------------
var favourites = await store.SearchGamesAsync(new GameQuery { Favorite = true }, CancellationToken.None);
Assert.Equal("Alpha Adventure", Assert.Single(favourites.Items).Title);
// The total describes the filtered population, not the whole catalog.
Assert.Equal(1, favourites.Total);
Assert.Equal(3, (await store.SearchGamesAsync(new GameQuery(), CancellationToken.None)).Total);
Assert.Equal("Alpha Adventure", Assert.Single((await store.SearchGamesAsync(
new GameQuery { Status = GamePlayStatus.Playing }, CancellationToken.None)).Items).Title);
Assert.Equal("Bravo Blast", Assert.Single((await store.SearchGamesAsync(
new GameQuery { Status = GamePlayStatus.Completed }, CancellationToken.None)).Items).Title);
Assert.Equal("Alpha Adventure", Assert.Single((await store.SearchGamesAsync(
new GameQuery { MinimumRating = 4 }, CancellationToken.None)).Items).Title);
Assert.Equal(2, (await store.SearchGamesAsync(new GameQuery { MinimumRating = 3 }, CancellationToken.None)).Total);
Assert.Equal("Charlie Chase", Assert.Single((await store.SearchGamesAsync(
new GameQuery { Tag = "Co-op" }, CancellationToken.None)).Items).Title);
var collection = await store.CreateCollectionAsync(new("Weekend picks", null, CollectionKind.Static, null, false),
"integration-test", CancellationToken.None);
await store.AddCollectionGameAsync(collection.Id, bravo.Id, "integration-test", CancellationToken.None);
Assert.Equal("Bravo Blast", Assert.Single((await store.SearchGamesAsync(
new GameQuery { CollectionId = collection.Id }, CancellationToken.None)).Items).Title);
// --- ordering --------------------------------------------------------------------------
var byRating = await store.SearchGamesAsync(new GameQuery { Sort = "rating" }, CancellationToken.None);
Assert.Equal(["Alpha Adventure", "Bravo Blast", "Charlie Chase"], byRating.Items.Select(item => item.Title));
var byPlayed = await store.SearchGamesAsync(new GameQuery { Sort = "played" }, CancellationToken.None);
Assert.Equal(["Bravo Blast", "Alpha Adventure", "Charlie Chase"], byPlayed.Items.Select(item => item.Title));
// A page boundary cannot change the answer, which page-local sorting could not guarantee.
var firstPage = await store.SearchGamesAsync(new GameQuery { Sort = "title", PageSize = 1 }, CancellationToken.None);
var secondPage = await store.SearchGamesAsync(new GameQuery { Sort = "title", PageSize = 1, Page = 2 }, CancellationToken.None);
Assert.Equal("Alpha Adventure", Assert.Single(firstPage.Items).Title);
Assert.Equal("Bravo Blast", Assert.Single(secondPage.Items).Title);
Assert.Equal(3, firstPage.Total);
// --- playable copies -------------------------------------------------------------------
Assert.Empty((await store.SearchGamesAsync(new GameQuery { PlayableOnly = true }, CancellationToken.None)).Items);
await LinkArtifactAsync(store, dataSource, library.Id, charlie.Id, "roms/snes/Charlie Chase.sfc", "snes");
await LinkArtifactAsync(store, dataSource, library.Id, bravo.Id, "roms/snes/Bravo Blast.txt", "snes");
// Only the allowlisted platform-and-container pair counts as a playable copy.
Assert.Equal("Charlie Chase", Assert.Single((await store.SearchGamesAsync(
new GameQuery { PlayableOnly = true }, CancellationToken.None)).Items).Title);
// --- scoped releases -------------------------------------------------------------------
await store.CreateReleaseAsync(alpha.Id, "Alpha Adventure", "snes", null, null, "integration-test", CancellationToken.None);
await store.CreateReleaseAsync(bravo.Id, "Bravo Blast", "snes", null, null, "integration-test", CancellationToken.None);
Assert.Equal("Alpha Adventure", Assert.Single(
await store.ListReleasesForGamesAsync([alpha.Id], CancellationToken.None)).Title);
Assert.Empty(await store.ListReleasesForGamesAsync([], CancellationToken.None));
Assert.Equal(2, (await store.ListReleasesAsync(null, CancellationToken.None)).Count);
}
private static async Task LinkArtifactAsync(PostgresStore store, NpgsqlDataSource dataSource,
Guid libraryId, Guid gameId, string relativePath, string platform)
{
var artifact = new Artifact(Guid.NewGuid(), libraryId, relativePath, 4 * 1024 * 1024,
DateTimeOffset.UtcNow, ArtifactState.Present, MediaType.Rom, Confidence.Deterministic, null,
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, null, platform);
await store.UpsertArtifactAsync(artifact, null, [], CancellationToken.None);
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(gameId);
link.Parameters.AddWithValue(artifact.Id);
await link.ExecuteNonQueryAsync();
}
}