using Flaro.Core.Metadata; using Microsoft.Extensions.Logging.Abstractions; namespace Flaro.Metadata.Tests; /// Die beiden Werte von Rotten Tomatoes, von der Seite selbst gelesen. /// /// /// Gegen einen festgehaltenen Seitenausschnitt, nicht gegen das Netz. /// Eine Prüfung, die wirklich hinausgeht, misst das Netz und nicht den Code; /// sie wäre launisch und fiele aus, sobald jemand ohne Verbindung arbeitet. /// Der Ausschnitt unten ist gekürzt, aber in der Form, die /// /m/the_matrix am 9. Oktober 2026 geliefert hat. /// public class RottenTomatoesTests { private static string Seite( string art = "Movie", string name = "The Matrix", string datum = "1999-03-31", string kritik = "83", string publikum = "85") => $$$""" """; private static RottenTomatoesClient Client(Attrappe attrappe, bool an = true) => new(new HttpClient(attrappe) { BaseAddress = new Uri(RottenTomatoesClient.BaseAddress) }, new FesterTomatenschalter(an), // Ohne Pause: die Bremse ist eine Höflichkeit gegenüber der // echten Seite, und eine Prüfung geht nirgends hin. new Anklopfbremse(TimeSpan.Zero), NullLogger.Instance); private static Wertungsfrage Matrix => new("tt0133093", "The Matrix", 1999); /// Ausgeschaltet heisst: keine einzige Anfrage. /// /// /// Das ist der wichtigste Satz dieser Datei. Hier wird eine fremde /// Webseite ausgelesen; das darf nicht beiläufig geschehen, weil jemand /// einen Scan startet. /// [Test] public async Task Ausgeschaltet_geht_keine_Anfrage_hinaus() { var attrappe = Attrappe.MitJson(Seite()); var wertungen = await Client(attrappe, an: false).HolenAsync(Matrix); Assert.Multiple(() => { Assert.That(wertungen, Is.Empty); Assert.That(attrappe.Aufrufe, Is.Empty); }); } /// Kritik und Publikum kommen getrennt zurück. [Test] public async Task Beide_Werte_kommen_getrennt() { var wertungen = await Client(Attrappe.MitJson(Seite())).HolenAsync(Matrix); Assert.Multiple(() => { var kritik = wertungen.Single(w => w.Quelle == "rottenTomatoes"); Assert.That(kritik.Anzeige, Is.EqualTo("83%")); Assert.That(kritik.Wert, Is.EqualTo(8.3).Within(0.01)); var publikum = wertungen.Single(w => w.Quelle == "rottenTomatoesAudience"); Assert.That(publikum.Anzeige, Is.EqualTo("85%")); Assert.That(publikum.Wert, Is.EqualTo(8.5).Within(0.01)); }); } /// Der gewöhnliche Titel kostet genau eine Anfrage. /// /// /// Gemessen an der echten Seite: /m/the_matrix antwortet, /// /m/the_matrix_1999 mit 404. Mit Jahr zuerst zu fragen kostete /// bei jedem gewöhnlichen Titel eine Anfrage zu viel — auf einer Seite, /// die man ohnehin nur mitbenutzt. /// [Test] public async Task Ein_gewoehnlicher_Titel_kostet_eine_Anfrage() { var attrappe = Attrappe.MitJson(Seite()); await Client(attrappe).HolenAsync(Matrix); Assert.Multiple(() => { Assert.That(attrappe.Aufrufe, Has.Count.EqualTo(1)); Assert.That(attrappe.Aufrufe[0], Does.EndWith("m/the_matrix")); }); } /// Ein Film mit anderem Jahr ist ein anderer Film. /// /// /// Auf der Seite steht keine IMDb-Kennung. Der Weg wird aus dem /// Titel geraten, und ein geratener Weg kann überall hinführen. Lieber /// keine Zahl als die eines fremden Films. /// [Test] public async Task Ein_fremdes_Jahr_wird_verworfen() { var attrappe = Attrappe.MitJson(Seite(name: "The Lion King", datum: "1994-06-24")); var wertungen = await Client(attrappe) .HolenAsync(new Wertungsfrage(null, "The Lion King", 2019)); Assert.That(wertungen, Is.Empty); } /// Beim falschen Jahr wird der Weg mit Jahr nachgereicht. /// /// /// Die Neuverfilmung ist der einzige Fall, der zwei Anfragen /// rechtfertigt. /m/the_lion_king ist die von 1994, /// /m/the_lion_king_2019 die neue. Erst wird die alte am Jahr /// erkannt und verworfen, dann greift die zweite. /// [Test] public async Task Eine_Neuverfilmung_wird_ueber_den_Weg_mit_Jahr_gefunden() { var attrappe = Attrappe.NachPfad( ("the_lion_king_2019", Seite(name: "The Lion King (2019)", datum: "2019-07-19", kritik: "52", publikum: "88")), ("the_lion_king", Seite(name: "The Lion King", datum: "1994-06-24", kritik: "93", publikum: "93"))); var wertungen = await Client(attrappe) .HolenAsync(new Wertungsfrage(null, "The Lion King", 2019)); Assert.Multiple(() => { Assert.That(attrappe.Aufrufe, Has.Count.EqualTo(2)); Assert.That(wertungen.Single(w => w.Quelle == "rottenTomatoes").Anzeige, Is.EqualTo("52%"), "die Fassung von 2019, nicht die von 1994"); Assert.That(wertungen.Single(w => w.Quelle == "rottenTomatoesAudience").Anzeige, Is.EqualTo("88%")); }); } /// Ein Jahr daneben ist kein anderer Film. /// /// /// Rotten Tomatoes führt das Datum der amerikanischen Erstaufführung, /// TMDB oft das der Heimatpremiere — ein Film vom Dezember steht dort /// schon mal im Folgejahr. /// [Test] public async Task Ein_Jahr_Unterschied_zaehlt_noch_als_derselbe() { var wertungen = await Client(Attrappe.MitJson(Seite(datum: "2000-01-20"))) .HolenAsync(Matrix); Assert.That(wertungen, Has.Count.EqualTo(2)); } /// Eine Serie darf nicht auf einer Filmseite landen. [Test] public async Task Eine_Filmseite_beantwortet_keine_Seriensuche() { var wertungen = await Client(Attrappe.MitJson(Seite(art: "Movie", name: "Dark"))) .HolenAsync(new Wertungsfrage(null, "Dark", 1999, Serie: true)); Assert.That(wertungen, Is.Empty); } [Test] public async Task Eine_Serie_wird_unter_tv_gesucht() { var attrappe = Attrappe.MitJson( Seite(art: "TVSeries", name: "Dark", datum: "2017-12-01")); var wertungen = await Client(attrappe) .HolenAsync(new Wertungsfrage(null, "Dark", 2017, Serie: true)); Assert.Multiple(() => { Assert.That(attrappe.Aufrufe[0], Does.EndWith("tv/dark"), "Serien stehen dort ohne Jahr: /tv/dark_2017 antwortet mit 404."); Assert.That(wertungen, Has.Count.EqualTo(2)); }); } /// Ein unbekannter Weg endet mit 404 — und das ist eine Antwort. [Test] public async Task Ein_unbekannter_Weg_ergibt_nichts() { var wertungen = await Client(Attrappe.MitStatus(System.Net.HttpStatusCode.NotFound)) .HolenAsync(Matrix); Assert.That(wertungen, Is.Empty); } /// Eine Sperrseite ohne die bekannten Felder ergibt nichts, keinen Absturz. [Test] public async Task Eine_umgebaute_Seite_ergibt_nichts() { var wertungen = await Client(Attrappe.MitJson("Nichts hier.")) .HolenAsync(Matrix); Assert.That(wertungen, Is.Empty); } [Test] public async Task Kein_Netz_ergibt_nichts_statt_eines_Absturzes() { var wertungen = await Client(Attrappe.DieWirft(new HttpRequestException("weg"))) .HolenAsync(Matrix); Assert.That(wertungen, Is.Empty); } /// Der Originaltitel wird zuerst versucht. /// /// /// Sonst fände ein deutscher Bestand dort so gut wie nichts. Steht /// die Mediathek auf Deutsch, heisst der Film bei uns „Ich einfach /// unverbesserlich"; Rotten Tomatoes kennt nur despicable_me. /// [Test] public async Task Der_Originaltitel_wird_zuerst_versucht() { var attrappe = Attrappe.NachPfad( ("despicable_me", Seite(name: "Despicable Me", datum: "2010-07-09", kritik: "81", publikum: "84"))); var wertungen = await Client(attrappe).HolenAsync(new Wertungsfrage( null, "Ich einfach unverbesserlich", 2010, Originaltitel: "Despicable Me")); Assert.Multiple(() => { Assert.That(attrappe.Aufrufe[0], Does.EndWith("m/despicable_me")); Assert.That(wertungen.Single(w => w.Quelle == "rottenTomatoesAudience").Anzeige, Is.EqualTo("84%")); }); } /// Gibt es den Originaltitel dort nicht, kommt der deutsche dran. [Test] public async Task Ohne_Treffer_wird_auch_der_andere_Titel_versucht() { var attrappe = Attrappe.NachPfad( ("ich_einfach_unverbesserlich", Seite(name: "Ich einfach unverbesserlich", datum: "2010-07-09"))); var wertungen = await Client(attrappe).HolenAsync(new Wertungsfrage( null, "Ich einfach unverbesserlich", 2010, Originaltitel: "Despicable Me")); Assert.Multiple(() => { Assert.That(attrappe.Aufrufe[0], Does.Contain("despicable_me"), "der originale zuerst"); Assert.That(wertungen, Has.Count.EqualTo(2)); }); } /// Derselbe Titel zweimal ergibt nicht zwei Anfragen. [Test] public async Task Ein_gleicher_Originaltitel_kostet_nichts_extra() { var attrappe = Attrappe.MitJson(Seite()); await Client(attrappe).HolenAsync( new Wertungsfrage(null, "The Matrix", 1999, Originaltitel: "The Matrix")); Assert.That(attrappe.Aufrufe, Has.Count.EqualTo(1)); } /// Einmal wirklich hinaus — nur von Hand. /// /// /// /// [Explicit], und das ist der Punkt. Diese Prüfung misst /// das Netz und eine fremde Seite, nicht den Code; im Durchlauf wäre sie /// launisch und fiele ohne Verbindung aus. Von Hand ist sie dagegen das /// einzige, was die Frage beantwortet, ob das Auslesen heute noch geht: /// /// /// dotnet test --filter "FullyQualifiedName~Wirklich_von_der_echten_Seite" /// /// [Test, Explicit("Geht wirklich ins Netz.")] public async Task Wirklich_von_der_echten_Seite_gelesen() { using var echt = new HttpClient { BaseAddress = new Uri(RottenTomatoesClient.BaseAddress) }; echt.DefaultRequestHeaders.UserAgent.ParseAdd( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/131.0.0.0 Safari/537.36"); var client = new RottenTomatoesClient( echt, new FesterTomatenschalter(true), new Anklopfbremse(TimeSpan.Zero), NullLogger.Instance); var wertungen = await client.HolenAsync(Matrix); Assert.Multiple(() => { Assert.That(wertungen.Select(w => w.Quelle), Is.EquivalentTo(new[] { "rottenTomatoes", "rottenTomatoesAudience" })); TestContext.Out.WriteLine(string.Join(", ", wertungen.Select(w => $"{w.Quelle} {w.Anzeige}"))); }); } /// So baut Rotten Tomatoes seine Wege. [TestCase("The Matrix", ExpectedResult = "the_matrix")] [TestCase("Spider-Man: No Way Home", ExpectedResult = "spider_man_no_way_home")] [TestCase("WALL·E", ExpectedResult = "wall_e")] [TestCase("Ich einfach unverbesserlich", ExpectedResult = "ich_einfach_unverbesserlich")] [TestCase("Österreich – Grüße", ExpectedResult = "osterreich_grusse")] [TestCase(" ", ExpectedResult = "")] public string Aus_dem_Titel_wird_ein_Weg(string titel) => RottenTomatoesClient.Kennstueck(titel); } public sealed record FesterTomatenschalter(bool An) : IRottenTomatoesSettingsSource { public Task EingeschaltetAsync(CancellationToken ct = default) => Task.FromResult(An); }