Files
PLib/tests/PLib.Tests/Metadata/StashBoxPayloadTests.cs
T

247 lines
9.0 KiB
C#

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.Select(x => x.Name).ShouldBe(["Студия"]);
first.Tags.Select(x => x.Name).ShouldBe(["драма", "нуар"]);
// The credited alias is not the person; the label has to be the performer's own name.
first.Performers.Select(x => x.Name).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 The_smallest_picture_still_wide_enough_for_a_card_is_the_one_kept()
{
// stash-box returns every size it holds, and the first is not the best: originals run
// to several thousand pixels, and one of those per performer to draw it 150 wide would
// cost megabytes a head.
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{
"id": "abc",
"title": "Сцена",
"studio": { "name": "Студия", "images": [ { "url": "s/4000.jpg", "width": 4000 }, { "url": "s/500.jpg", "width": 500 } ] },
"tags": [ { "name": "драма" } ],
"performers": [
{ "performer": { "name": "Актёр", "images": [
{ "url": "p/2000.jpg", "width": 2000 },
{ "url": "p/400.jpg", "width": 400 },
{ "url": "p/100.jpg", "width": 100 } ] } }
]
}
]
}
}
""";
var match = Read(payload, Flat).ShouldHaveSingleItem();
match.Performers.Single().ImageUrl.ShouldBe("p/400.jpg");
match.Studios.Single().ImageUrl.ShouldBe("s/500.jpg");
// stash-box holds no picture for a tag at all, so there is nothing to find.
match.Tags.Single().ImageUrl.ShouldBeNull();
}
[Fact]
public void The_scene_carries_its_own_cover_apart_from_the_pictures_of_its_people()
{
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{
"id": "abc",
"title": "Сцена",
"images": [ { "url": "scene/1920.jpg", "width": 1920 }, { "url": "scene/640.jpg", "width": 640 } ],
"performers": [ { "performer": { "name": "Актёр", "images": [ { "url": "p/400.jpg", "width": 400 } ] } } ]
}
]
}
}
""";
var match = Read(payload, Flat).ShouldHaveSingleItem();
match.ImageUrl.ShouldBe("scene/640.jpg");
match.Performers.Single().ImageUrl.ShouldBe("p/400.jpg");
// Still a URL, and it stays one: whether to spend the bandwidth is the caller's
// decision, and a candidate must never wait on a picture host to be shown.
match.ImageUrl.ShouldNotBeNull();
}
[Fact]
public void When_every_picture_is_too_small_the_widest_is_taken_rather_than_none()
{
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{
"id": "abc",
"title": "Сцена",
"performers": [
{ "performer": { "name": "Актёр", "images": [
{ "url": "p/80.jpg", "width": 80 },
{ "url": "p/200.jpg", "width": 200 } ] } }
]
}
]
}
}
""";
// A small picture beats an empty card.
Read(payload, Flat).Single().Performers.Single().ImageUrl.ShouldBe("p/200.jpg");
}
[Fact]
public void An_entity_with_no_pictures_at_all_is_still_a_match()
{
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{
"id": "abc",
"title": "Сцена",
"studio": { "name": "Студия", "images": [] },
"performers": [ { "performer": { "name": "Актёр" } } ]
}
]
}
}
""";
var match = Read(payload, Flat).ShouldHaveSingleItem();
match.Studios.Single().ImageUrl.ShouldBeNull();
match.Performers.Single().Name.ShouldBe("Актёр");
}
[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");
}
}