Update README.md to reflect changes in stash-box schema and query handling. Introduce new GraphQL queries for scene retrieval by fingerprints in StashBoxMetadataProvider, enhancing metadata lookup capabilities. Add InternalsVisibleTo attribute for testing access in PLib.Infrastructure project.

This commit is contained in:
Leonid Pershin
2026-08-09 08:34:17 +03:00
parent eb4894428b
commit 61d01d1970
4 changed files with 313 additions and 42 deletions
@@ -0,0 +1,133 @@
using System.Text.Json;
using PLib.Infrastructure.Metadata;
using Shouldly;
namespace PLib.Tests.Metadata;
/// <summary>
/// Decoding a stash-box reply, against payloads shaped like the real ones.
/// </summary>
/// <remarks>
/// This is the part of the feature that cannot be checked without a server and a key, and the
/// part where a wrong guess looks exactly like "the source knows nothing about your video".
/// </remarks>
public sealed class StashBoxPayloadTests
{
/// <summary>The newer query, whose result is one group of scenes per group of fingerprints.</summary>
private static StashBoxMetadataProvider.GraphQlQuery Nested => StashBoxMetadataProvider.Queries[0];
/// <summary>The older query, kept for instances that have not been updated.</summary>
private static StashBoxMetadataProvider.GraphQlQuery Flat => StashBoxMetadataProvider.Queries[1];
[Fact]
public void The_newer_query_returns_scenes_one_level_deeper_and_they_are_flattened()
{
const string payload = """
{
"data": {
"findScenesBySceneFingerprints": [
[
{
"id": "abc",
"title": "Первая",
"details": "Описание",
"studio": { "name": "Студия" },
"tags": [ { "name": "драма" }, { "name": "нуар" } ],
"performers": [ { "performer": { "name": "Актёр" }, "as": "Псевдоним" } ]
},
{ "id": "def", "title": "Вторая", "studio": null, "tags": [], "performers": [] }
]
]
}
}
""";
var matches = Read(payload, Nested);
matches.Count.ShouldBe(2);
var first = matches[0];
first.SourceName.ShouldBe("StashDB");
first.RemoteId.ShouldBe("abc");
first.Title.ShouldBe("Первая");
first.Description.ShouldBe("Описание");
first.Studios.ShouldBe(["Студия"]);
first.Tags.ShouldBe(["драма", "нуар"]);
// The credited alias is not the person; the label has to be the performer's own name.
first.Performers.ShouldBe(["Актёр"]);
matches[1].Studios.ShouldBeEmpty();
}
[Fact]
public void The_older_query_returns_a_flat_list()
{
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{ "id": "abc", "title": "Сцена", "studio": { "name": "Студия" }, "tags": [], "performers": [] }
]
}
}
""";
Read(payload, Flat).ShouldHaveSingleItem().Title.ShouldBe("Сцена");
}
[Fact]
public void A_reply_with_no_matches_is_an_empty_list_rather_than_a_failure()
{
Read("""{ "data": { "findScenesBySceneFingerprints": [ [] ] } }""", Nested).ShouldBeEmpty();
Read("""{ "data": { "findScenesBySceneFingerprints": null } }""", Nested).ShouldBeEmpty();
}
[Fact]
public void A_scene_without_a_title_still_produces_a_match_that_can_be_shown()
{
var match = Read(
"""{ "data": { "findSceneByFingerprint": [ { "id": "abc", "title": null } ] } }""",
Flat).ShouldHaveSingleItem();
match.Title.ShouldNotBeNullOrWhiteSpace();
match.Tags.ShouldBeEmpty();
}
[Fact]
public void Errors_are_raised_even_though_the_server_answered_two_hundred()
{
// A rejected key comes back as 200 with an errors array; treating that as "nothing
// found" would tell the user their video is unknown rather than their key is wrong.
var payload = """{ "errors": [ { "message": "unauthorized" } ], "data": null }""";
Should.Throw<InvalidOperationException>(() => Read(payload, Nested))
.Message.ShouldBe("unauthorized");
}
[Fact]
public void A_missing_field_is_reported_apart_from_other_errors_so_the_older_query_gets_a_turn()
{
// Verbatim from a live instance that only has the newer query.
var payload = """
{
"errors": [
{ "message": "Cannot query field \"findSceneByFingerprint\" on type \"Query\". Did you mean \"findScenesBySceneFingerprints\"?" }
]
}
""";
var rejected = Should.Throw<Exception>(() => Read(payload, Flat));
// Not the type the caller reports as a failure: this one means "try the next query".
rejected.ShouldNotBeOfType<InvalidOperationException>();
}
private static IReadOnlyList<PLib.Application.Metadata.VideoMetadataMatch> Read(
string payload,
StashBoxMetadataProvider.GraphQlQuery query)
{
using var document = JsonDocument.Parse(payload);
return StashBoxMetadataProvider.ReadPayload(document.RootElement, query, "StashDB");
}
}