using System.Net;
using Flaro.Core.Metadata;
using Microsoft.Extensions.Logging.Abstractions;
namespace Flaro.Metadata.Tests;
/// Bewertungen von IMDb, Rotten Tomatoes und Metacritic.
///
///
/// Warum OMDb und nicht die Quellen selbst. IMDbs eigene
/// Schnittstelle kostet Geld, Rotten Tomatoes vergibt Zugänge nur an
/// Partner, Letterboxd hat gar keine offene. OMDb liefert alle drei in
/// einer Antwort, über die IMDb-Kennung, die Flaro ohnehin schon führt.
///
public class OmdbTests
{
/// Wonach gefragt wird — bei OMDb zählt allein die Kennung.
private static Wertungsfrage Frage(string imdbId) => new(imdbId, "Probe", 1999);
private static OmdbClient Client(Attrappe attrappe, string? schluessel = "ein-schluessel") =>
new(new HttpClient(attrappe) { BaseAddress = new Uri(OmdbClient.BaseAddress) },
new FesterOmdbSchluessel(schluessel),
NullLogger.Instance);
private const string Antwort = """
{"Title":"Probe","Response":"True",
"Ratings":[
{"Source":"Internet Movie Database","Value":"7.4/10"},
{"Source":"Rotten Tomatoes","Value":"87%"},
{"Source":"Metacritic","Value":"62/100"}]}
""";
[Test]
public async Task Ohne_Schluessel_geht_keine_Anfrage_hinaus()
{
var attrappe = Attrappe.MitJson(Antwort);
var wertungen = await Client(attrappe, schluessel: null).HolenAsync(Frage("tt0000001"));
Assert.Multiple(() =>
{
Assert.That(wertungen, Is.Empty);
Assert.That(attrappe.Aufrufe, Is.Empty);
});
}
/// Jede Quelle kommt mit zwei Werten: einem zum Vergleichen, einem zum Anzeigen.
///
///
/// Rotten Tomatoes in „8,7 von 10" umzurechnen wäre eine Lüge über
/// die Quelle. Dort steht 87 %, und genau so kennt man es. Zum
/// Sortieren braucht es trotzdem eine gemeinsame Skala — deshalb beides.
///
[Test]
public async Task Alle_drei_Quellen_kommen_normiert_und_lesbar()
{
var wertungen = await Client(Attrappe.MitJson(Antwort)).HolenAsync(Frage("tt0000001"));
Assert.Multiple(() =>
{
var imdb = wertungen.Single(w => w.Quelle == "imdb");
Assert.That(imdb.Wert, Is.EqualTo(7.4).Within(0.01));
Assert.That(imdb.Anzeige, Is.EqualTo("7,4"));
var rt = wertungen.Single(w => w.Quelle == "rottenTomatoes");
Assert.That(rt.Wert, Is.EqualTo(8.7).Within(0.01));
Assert.That(rt.Anzeige, Is.EqualTo("87%"), "so steht es dort, und so kennt man es");
var mc = wertungen.Single(w => w.Quelle == "metacritic");
Assert.That(mc.Wert, Is.EqualTo(6.2).Within(0.01));
Assert.That(mc.Anzeige, Is.EqualTo("62"));
});
}
/// OMDb antwortet auf Unbekanntes mit 200 und „Response: False".
///
///
/// Das als Treffer durchzulassen hiesse, eine leere Bewertung abzulegen
/// — und die stünde danach als „keine Wertung" da, obwohl es nur keine
/// Antwort gab.
///
[Test]
public async Task Eine_abschlaegige_Antwort_ergibt_nichts()
{
var json = """{"Response":"False","Error":"Incorrect IMDb ID."}""";
Assert.That(await Client(Attrappe.MitJson(json)).HolenAsync(Frage("tt0000001")), Is.Empty);
}
/// Eine unbekannte vierte Quelle wird übergangen, nicht geraten.
[Test]
public async Task Eine_fremde_Quelle_wird_uebergangen()
{
var json = """
{"Response":"True","Ratings":[
{"Source":"Irgendwas Neues","Value":"5/5"},
{"Source":"Internet Movie Database","Value":"7.4/10"}]}
""";
var wertungen = await Client(Attrappe.MitJson(json)).HolenAsync(Frage("tt0000001"));
Assert.That(wertungen.Select(w => w.Quelle), Is.EquivalentTo(new[] { "imdb" }));
}
[Test]
public async Task Kein_Netz_ergibt_nichts_statt_eines_Absturzes()
{
var wertungen = await Client(Attrappe.DieWirft(new HttpRequestException("weg")))
.HolenAsync(Frage("tt0000001"));
Assert.That(wertungen, Is.Empty);
}
[Test]
public async Task Ein_Fehlercode_ergibt_nichts()
{
Assert.That(
await Client(Attrappe.MitStatus(HttpStatusCode.Unauthorized)).HolenAsync(Frage("tt0000001")),
Is.Empty);
}
[Test]
public async Task Kennung_und_Schluessel_stehen_in_der_Anfrage()
{
var attrappe = Attrappe.MitJson(Antwort);
await Client(attrappe).HolenAsync(Frage("tt0133093"));
Assert.That(attrappe.Aufrufe[0],
Does.Contain("i=tt0133093").And.Contain("apikey=ein-schluessel"));
}
/// Der Publikumswert wird mitverlangt — er kostet nichts extra.
///
///
/// Eine Anfrage, nicht zwei. OMDb legt die zusätzlichen Felder
/// derselben Antwort bei; das Tageskontingent von tausend Zugriffen
/// bleibt davon unberührt.
///
[Test]
public async Task Der_Publikumswert_wird_mitverlangt()
{
var attrappe = Attrappe.MitJson(Antwort);
await Client(attrappe).HolenAsync(Frage("tt0133093"));
Assert.That(attrappe.Aufrufe, Has.Count.EqualTo(1));
Assert.That(attrappe.Aufrufe[0], Does.Contain("tomatoes=true"));
}
/// Steht eine Zahl im Publikumsfeld, wird sie eine eigene Wertung.
///
///
/// Getrennt von der Kritik, nicht vermischt. Bei Rotten Tomatoes
/// liegen die beiden Zahlen oft weit auseinander; sie unter einem Namen
/// zu führen hiesse, dem Betrachter zu verschweigen, welche er sieht.
///
[Test]
public async Task Ein_Publikumswert_kommt_getrennt_von_der_Kritik()
{
var json = """
{"Response":"True",
"Ratings":[{"Source":"Rotten Tomatoes","Value":"83%"}],
"tomatoUserMeter":"88"}
""";
var wertungen = await Client(Attrappe.MitJson(json)).HolenAsync(Frage("tt0000001"));
Assert.Multiple(() =>
{
var kritik = wertungen.Single(w => w.Quelle == "rottenTomatoes");
Assert.That(kritik.Anzeige, Is.EqualTo("83%"));
var publikum = wertungen.Single(w => w.Quelle == "rottenTomatoesAudience");
Assert.That(publikum.Anzeige, Is.EqualTo("88%"));
Assert.That(publikum.Wert, Is.EqualTo(8.8).Within(0.01));
});
}
/// N/A ist keine Wertung, sondern deren Fehlen.
///
///
/// Und das ist bei OMDb der Normalfall. Seit 2018 wird berichtet,
/// dass die erweiterten Rotten-Tomatoes-Felder leer bleiben; die Frage,
/// ob sie je wieder Daten führen, ist seit 2017 offen. Eine „0 %"
/// daraus zu machen wäre eine Behauptung über einen Film.
///
[TestCase("\"N/A\"")]
[TestCase("\"\"")]
[TestCase("null")]
[TestCase("\"120\"")]
public async Task Ohne_brauchbaren_Publikumswert_entsteht_keine_Wertung(string feld)
{
var json = $$"""
{"Response":"True",
"Ratings":[{"Source":"Rotten Tomatoes","Value":"83%"}],
"tomatoUserMeter":{{feld}}}
""";
var wertungen = await Client(Attrappe.MitJson(json)).HolenAsync(Frage("tt0000001"));
Assert.Multiple(() =>
{
Assert.That(wertungen.Select(w => w.Quelle), Is.EquivalentTo(new[] { "rottenTomatoes" }));
Assert.That(wertungen.Single().Anzeige, Is.EqualTo("83%"),
"die Kritik bleibt, auch wenn das Publikum fehlt");
});
}
/// Ein Zahlenfeld darf nicht die ganze Antwort verwerfen.
///
///
/// Fest auf Text getippt wäre das ein Lesefehler — und der
/// verwirft nicht bloss dieses Feld, sondern die drei Bewertungen
/// daneben. OMDb schreibt hier bisher Text; darauf zu wetten ist
/// unnötig.
///
[Test]
public async Task Ein_Publikumswert_als_Zahl_wird_auch_gelesen()
{
var json = """
{"Response":"True",
"Ratings":[{"Source":"Internet Movie Database","Value":"7.4/10"}],
"tomatoUserMeter":88}
""";
var wertungen = await Client(Attrappe.MitJson(json)).HolenAsync(Frage("tt0000001"));
Assert.That(wertungen.Select(w => w.Quelle),
Is.EquivalentTo(new[] { "imdb", "rottenTomatoesAudience" }));
}
/// Ohne IMDb-Kennung wird gar nicht erst gefragt.
///
///
/// OMDb kennt nur diesen Weg; nach einem Titel zu suchen hiesse, denselben
/// Zuordnungsfehler ein zweites Mal zu riskieren.
///
[Test]
public async Task Ohne_Kennung_geht_keine_Anfrage_hinaus()
{
var attrappe = Attrappe.MitJson(Antwort);
await Client(attrappe).HolenAsync(Frage(""));
Assert.That(attrappe.Aufrufe, Is.Empty);
}
}
public sealed record FesterOmdbSchluessel(string? Wert) : IOmdbSettingsSource
{
public Task SchluesselAsync(CancellationToken ct = default) => Task.FromResult(Wert);
}