122 lines
7.6 KiB
C#
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();
|
|
}
|
|
}
|