using System.Net;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
using Flaro.Core.Metadata;
using Microsoft.Extensions.DependencyInjection;
namespace Flaro.Api.Tests;
/// Ein Anbieter, der antwortet, was der Test vorgibt.
public sealed class VorgegebenerAnbieter : IMetadataProvider
{
public bool Konfiguriert { get; set; } = true;
public Exception? Wirft { get; set; }
public MovieMetadata? Film { get; set; }
public SeriesMetadata? Serie { get; set; }
public EpisodeMetadata? Folge { get; set; }
public List Gefragt { get; } = [];
public Task IsConfiguredAsync(CancellationToken ct = default) => Task.FromResult(Konfiguriert);
public Task FindMovieAsync(string titel, int? jahr, CancellationToken ct = default)
{
Gefragt.Add($"film:{titel}:{jahr}");
if (Wirft is not null) throw Wirft;
return Task.FromResult(Film);
}
public Task FindSeriesAsync(string titel, int? jahr, CancellationToken ct = default)
{
Gefragt.Add($"serie:{titel}");
if (Wirft is not null) throw Wirft;
return Task.FromResult(Serie);
}
public Task FindEpisodeAsync(int serie, int staffel, int folge, CancellationToken ct = default)
{
Gefragt.Add($"folge:{serie}:{staffel}:{folge}");
if (Wirft is not null) throw Wirft;
return Task.FromResult(Folge);
}
public Task GetMovieAsync(int id, CancellationToken ct = default)
{
Gefragt.Add($"filmId:{id}");
if (Wirft is not null) throw Wirft;
return Task.FromResult(Film);
}
public Task GetSeriesAsync(int id, CancellationToken ct = default)
{
Gefragt.Add($"serieId:{id}");
if (Wirft is not null) throw Wirft;
return Task.FromResult(Serie);
}
public Task> SearchMoviesAsync(string t, int? j, CancellationToken ct = default)
{
// Mit Jahr: genau daran hing der Fehler, dass ein falscher Treffer
// seine eigene Korrektur verhinderte.
Gefragt.Add($"suchFilm:{t}:{j}");
return Task.FromResult>(Film is null ? [] : [Film]);
}
public Task> SearchSeriesAsync(string t, int? j, CancellationToken ct = default)
{
Gefragt.Add($"suchSerie:{t}");
return Task.FromResult>(Serie is null ? [] : [Serie]);
}
}
/// Eine Bildablage, die nichts herunterlädt.
public sealed class VorgegebeneBilder : IImageStore
{
public List Geholt { get; } = [];
public Task StoreAsync(RemoteImage bild, CancellationToken ct = default)
{
Geholt.Add(bild.Url);
return Task.FromResult(new string('a', 64));
}
}
public class MetadataTests
{
private string _bestand = "";
[SetUp]
public void Anlegen() => _bestand = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-meta-" + Guid.CreateVersion7().ToString("N"))).FullName;
[TearDown]
public void Aufraeumen()
{
try { Directory.Delete(_bestand, recursive: true); } catch (IOException) { }
}
private void Datei(string relativ)
{
var voll = Path.Combine(_bestand, relativ);
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
File.WriteAllBytes(voll, new byte[1024]);
}
private FlaroServer MitAnbieter(VorgegebenerAnbieter anbieter, VorgegebeneBilder? bilder = null) =>
new(dienste =>
{
dienste.AddScoped(_ => anbieter);
dienste.AddScoped(_ => bilder ?? new VorgegebeneBilder());
});
private async Task<(HttpClient Admin, Guid Library)> Einrichten(FlaroServer server, string art)
{
var admin = await server.SetUpAndSignIn();
var client = server.AlsAngemeldeter(admin.Token);
var angelegt = await client.PostAsJsonAsync("/api/v1/libraries",
new CreateLibraryRequest("Prüfbestand", art, _bestand));
angelegt.EnsureSuccessStatusCode();
return (client, (await angelegt.Content.ReadFromJsonAsync())!.Id);
}
private static async Task Scannen(HttpClient client, Guid bibliothek)
{
(await client.PostAsync($"/api/v1/libraries/{bibliothek}/scan", null)).EnsureSuccessStatusCode();
for (var i = 0; i < 100; i++)
{
var stand = await client.GetFromJsonAsync($"/api/v1/libraries/{bibliothek}/scan");
if (stand!.State == "finished") return stand;
if (stand.State == "failed") Assert.Fail($"Gescheitert: {stand.Error}");
await Task.Delay(50);
}
Assert.Fail("Nicht fertig geworden.");
return null!;
}
[Test]
public async Task Der_Titel_aus_der_Quelle_ersetzt_den_aus_dem_Dateinamen()
{
Datei("The.Matrix.1999.1080p.mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(603, "Matrix", 1999, "Neo zweifelt.",
new RemoteImage("https://bild/p.jpg"), new RemoteImage("https://bild/b.jpg")),
};
var bilder = new VorgegebeneBilder();
using var server = MitAnbieter(anbieter, bilder);
var (client, bibliothek) = await Einrichten(server, "Movies");
var stand = await Scannen(client, bibliothek);
Assert.That(stand.Metadata!.MoviesMatched, Is.EqualTo(1));
Assert.That(anbieter.Gefragt, Does.Contain("film:The Matrix:1999"));
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(filme!.Items[0].Title, Is.EqualTo("Matrix"));
Assert.That(filme.Items[0].Overview, Is.EqualTo("Neo zweifelt."));
Assert.That(filme.Items[0].PosterImage, Is.Not.Null);
Assert.That(bilder.Geholt, Is.EquivalentTo(new[] { "https://bild/p.jpg", "https://bild/b.jpg" }));
}
/// Zwei Namen, ein Film — das darf den Lauf nicht abbrechen.
///
///
///
/// Der Fehler, den der Eindeutigkeitsindex selbst verursacht hat.
/// Beim Scan heisst der eine Eintrag „Ich einfach unverbesserlich" und
/// der andere „Despicable Me"; dass es derselbe Film ist, verrät erst die
/// TMDB-Kennung. Wurde sie beiden zugewiesen, verletzte das die Regel —
/// und EF brach die gesamte Speicherung ab, mit „An error occurred while
/// saving the entity changes". Die Meldung nennt den Grund nicht und
/// nimmt den ganzen Lauf mit.
///
///
/// Hier antwortet der Anbieter auf jede Anfrage mit demselben Film — also
/// genau die Lage, die vorher den Lauf zerriss.
///
///
[Test]
public async Task Zwei_Namen_fuer_denselben_Film_werden_zusammengelegt()
{
Datei("Despicable Me (2010).mkv");
Datei("Ich einfach unverbesserlich (2010).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(20352, "Ich einfach unverbesserlich", 2010, "Gru hat einen Plan.",
null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
var stand = await Scannen(client, bibliothek);
Assert.That(stand.State, Is.EqualTo("finished"), "Der Lauf darf nicht abbrechen.");
Assert.That(stand.Metadata!.Error, Is.Null);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(filme!.Total, Is.EqualTo(1), "Ein Film, nicht zwei.");
Assert.That(filme.Items[0].Title, Is.EqualTo("Ich einfach unverbesserlich"));
Assert.That(filme.Items[0].Files, Has.Count.EqualTo(2),
"Beide Dateien hängen am überlebenden Eintrag — keine darf verlorengehen.");
}
/// Dasselbe für Serien.
[Test]
public async Task Zwei_Namen_fuer_dieselbe_Serie_werden_zusammengelegt()
{
Datei("Dark/Staffel 1/Dark - S01E01.mkv");
Datei("Dark (2017)/Staffel 1/Dark (2017) - S01E02.mkv");
var anbieter = new VorgegebenerAnbieter
{
Serie = new SeriesMetadata(70523, "Dark", 2017, "Eine Höhle.", null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Shows");
var stand = await Scannen(client, bibliothek);
Assert.That(stand.State, Is.EqualTo("finished"));
var serien = await client.GetFromJsonAsync>("/api/v1/series");
Assert.That(serien!.Total, Is.EqualTo(1));
Assert.That(serien.Items[0].EpisodeCount, Is.EqualTo(2),
"Beide Folgen hängen an der überlebenden Serie.");
}
/// Was die Quelle nicht kennt, bleibt trotzdem in der Mediathek.
/// „Metadaten auffrischen" muss den Titel wirklich fortschreiben.
///
///
/// Der teuerste Fehler der ganzen Sprachgeschichte. Die
/// Dublettenprüfung schlägt jeden Titel nach, der schon eine Kennung hat —
/// beim Auffrischen also auch den gerade bearbeiteten. Ohne den Vergleich
/// auf Gleichheit fand sich jeder zugeordnete Film als „Dublette seiner
/// selbst" und wurde übersprungen: Titel, Beschreibung und Sprache blieben
/// für immer so, wie sie beim ersten Lauf hereinkamen.
///
/// In der Oberfläche sah das aus, als wirke die Spracheinstellung nicht —
/// und genau so wurde es gemeldet: „eine Sprachänderung und anschliessendes
/// Neuholen bringt gar nichts".
///
[Test]
public async Task Auffrischen_schreibt_einen_zugeordneten_Titel_fort()
{
Datei("101 Dalmatians (1996).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(8741, "101 Dalmatians", 1996, "Englisch.", null, null)
{
Language = "en-US",
},
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var vorher = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(vorher!.Items[0].Title, Is.EqualTo("101 Dalmatians"));
// Dieselbe Kennung, andere Sprache — genau der Fall nach einer Umstellung.
anbieter.Film = new MovieMetadata(8741, "101 Dalmatiner", 1996, "Deutsch.", null, null)
{
Language = "de-DE",
};
(await client.PostAsync($"/api/v1/libraries/{bibliothek}/refresh", null))
.EnsureSuccessStatusCode();
for (var i = 0; i < 100; i++)
{
var stand = await client.GetFromJsonAsync($"/api/v1/libraries/{bibliothek}/scan");
if (stand!.State == "finished") break;
await Task.Delay(50);
}
var nachher = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(nachher!.Total, Is.EqualTo(1), "Es bleibt ein Film, keine Dublette.");
Assert.That(nachher.Items[0].Title, Is.EqualTo("101 Dalmatiner"));
Assert.That(nachher.Items[0].Overview, Is.EqualTo("Deutsch."));
}
/// Was von Hand benannt wurde, überlebt das Auffrischen.
[Test]
public async Task Ein_festgeschriebener_Titel_bleibt_beim_Auffrischen_stehen()
{
Datei("Irgendwas (2001).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(42, "Automatisch", 2001, null, null, null) { Language = "de-DE" },
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
var kennung = filme!.Items[0].Id;
(await client.PutAsJsonAsync($"/api/v1/movies/{kennung}/title", new { title = "Von Hand" }))
.EnsureSuccessStatusCode();
anbieter.Film = new MovieMetadata(42, "Automatisch, neu", 2001, null, null, null)
{
Language = "de-DE",
};
(await client.PostAsync($"/api/v1/libraries/{bibliothek}/refresh", null))
.EnsureSuccessStatusCode();
for (var i = 0; i < 100; i++)
{
var stand = await client.GetFromJsonAsync($"/api/v1/libraries/{bibliothek}/scan");
if (stand!.State == "finished") break;
await Task.Delay(50);
}
var nachher = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(nachher!.Items[0].Title, Is.EqualTo("Von Hand"));
}
/// Auffrischen nimmt eine Auswahl entgegen — und geht auch ohne.
///
///
/// Gemessen, nachdem es einmal brach. Als die Auswahl als
/// RefreshRequest? im Kopf der Methode stand, lehnte der Server
/// jede Anfrage ohne Rumpf mit 404 ab — also den Knopf „Metadaten neu
/// holen" für die ganze Mediathek, den es länger gibt als die Auswahl.
/// Beide Formen gehören deshalb in eine Prüfung.
///
[Test]
public async Task Auffrischen_nimmt_eine_Auswahl_entgegen_und_geht_auch_ohne()
{
Datei("101 Dalmatians (1996)/101 Dalmatians (1996).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(8741, "101 Dalmatiner", 1996, "Deutsch.", null, null)
{
Language = "de-DE",
},
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
var kennung = filme!.Items[0].Id;
// Ohne Rumpf: der alte Weg, und er muss weiter gehen.
var ohne = await client.PostAsync($"/api/v1/libraries/{bibliothek}/refresh", null);
Assert.That(ohne.StatusCode, Is.EqualTo(HttpStatusCode.Accepted),
"ein fehlender Rumpf heisst alles, nicht nicht-gefunden");
await Fertig(client, bibliothek);
// Mit Auswahl: nur dieser eine Titel.
var mit = await client.PostAsJsonAsync(
$"/api/v1/libraries/{bibliothek}/refresh", new RefreshRequest([kennung]));
Assert.That(mit.StatusCode, Is.EqualTo(HttpStatusCode.Accepted));
var stand = await Fertig(client, bibliothek);
Assert.Multiple(() =>
{
Assert.That(stand.Metadata?.MoviesMatched, Is.EqualTo(1));
Assert.That(stand.FilesFound, Is.Null,
"bei einer Auswahl wird nicht noch einmal das Dateisystem abgelaufen");
});
}
private static async Task Fertig(HttpClient client, Guid bibliothek)
{
for (var i = 0; i < 200; i++)
{
var stand = await client.GetFromJsonAsync($"/api/v1/libraries/{bibliothek}/scan");
if (stand!.State == "finished") return stand;
if (stand.State == "failed") Assert.Fail($"Gescheitert: {stand.Error}");
await Task.Delay(50);
}
Assert.Fail("Nicht fertig geworden.");
return null!;
}
/* ---------- Zwei Teile, ein Treffer ---------- */
/// Zwei Filme verschiedener Jahre werden nicht zusammengelegt.
///
///
///
/// Aus dem echten Bestand, und es kostete drei Filme. „Harry Potter
/// und die Heiligtümer des Todes Teil 1" und „Teil 2" landen bei TMDB auf
/// demselben Treffer, sobald ohne Jahr gesucht werden muss — gemessen:
/// die Suche liefert für beide Titel Teil 2 zuerst. Weil zwei
/// Einträge dieselbe Kennung nicht teilen dürfen, wurde der zweite als
/// „zweite Fassung" des ersten geschluckt. In der Übersicht standen dann
/// 448 Filme und 13 zweite Fassungen, in Plex 451 Filme — und nirgends
/// stand, welche drei fehlen.
///
///
/// Das Jahr aus dem Dateinamen ändert sich nie. Widersprechen sich zwei,
/// sind es verschiedene Filme, und dann bleibt der zweite lieber ohne
/// Zuordnung — sichtbar in der Liste der nicht zugeordneten Titel — als
/// lautlos verschwunden.
///
///
[Test]
public async Task Zwei_Teile_mit_verschiedenen_Jahren_bleiben_zwei_Filme()
{
Datei("Heiligtuemer des Todes Teil 1 (2010)/Heiligtuemer des Todes Teil 1 (2010).mkv");
Datei("Heiligtuemer des Todes Teil 2 (2011)/Heiligtuemer des Todes Teil 2 (2011).mkv");
// Die Quelle liefert für jede Suche denselben Film — genau wie TMDB
// es ohne Jahr tut.
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(12445, "Heiligtümer des Todes – Teil 2", 2011, null, null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(filme!.Items, Has.Count.EqualTo(2),
"zwei Dateien, zwei Jahre — also zwei Filme, nicht einer mit zwei Fassungen");
var ohne = await client.GetFromJsonAsync>(
$"/api/v1/libraries/{bibliothek}/unmatched");
Assert.That(ohne, Has.Count.EqualTo(1),
"der zweite bleibt ohne Zuordnung — und steht dafür in der Liste");
}
/// Derselbe Film in zwei Ordnern wird weiterhin zusammengelegt.
///
///
/// Die Gegenprobe, und sie ist wichtiger als sie aussieht. „Despicable
/// Me" und „Ich einfach unverbesserlich" sind wirklich derselbe Film —
/// gleiches Jahr, zwei Ordner. Diesen Fall mit zu verbieten hiesse, einen
/// Fehler durch einen anderen zu ersetzen.
///
[Test]
public async Task Derselbe_Film_in_zwei_Ordnern_wird_weiter_zusammengelegt()
{
Datei("Despicable Me (2010)/Despicable Me (2010).mkv");
Datei("Ich einfach unverbesserlich (2010)/Ich einfach unverbesserlich (2010).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(20352, "Ich einfach unverbesserlich", 2010, null, null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.Multiple(() =>
{
Assert.That(filme!.Items, Has.Count.EqualTo(1), "ein Film");
Assert.That(filme.Items[0].Files, Has.Count.EqualTo(2), "mit zwei Dateien");
});
}
/// Ohne Jahr im Namen wird weiter zusammengelegt.
///
///
/// Eine Lücke ist keine Aussage. Wer seine Dateien ohne Jahr
/// benennt, bekäme sonst nie eine Dublette erkannt — und zwei Ordner
/// desselben Films blieben für immer zwei Einträge.
///
[Test]
public async Task Ohne_Jahr_im_Namen_wird_weiter_zusammengelegt()
{
Datei("Despicable Me/Despicable Me.mkv");
Datei("Ich einfach unverbesserlich/Ich einfach unverbesserlich.mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(20352, "Ich einfach unverbesserlich", 2010, null, null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(filme!.Items, Has.Count.EqualTo(1));
}
/// Titel ohne Zuordnung stehen in einer Liste — mit Dateinamen.
[Test]
public async Task Nicht_zugeordnete_Filme_stehen_in_der_Liste()
{
Datei("Ein Mitschnitt ohne Titel/Ein Mitschnitt ohne Titel.mkv");
// Die Quelle findet nichts.
var anbieter = new VorgegebenerAnbieter { Film = null };
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var ohne = await client.GetFromJsonAsync>(
$"/api/v1/libraries/{bibliothek}/unmatched");
Assert.That(ohne, Has.Count.EqualTo(1));
Assert.Multiple(() =>
{
Assert.That(ohne![0].Art, Is.EqualTo("movie"));
Assert.That(ohne[0].Dateien, Is.EqualTo(1));
Assert.That(ohne[0].Beispiel, Does.EndWith(".mkv"));
Assert.That(ohne[0].Beispiel, Does.Not.Contain("/"), "nur der Dateiname, nicht der Pfad");
});
}
/// Eine falsche Zuordnung darf ihre eigene Korrektur nicht verhindern.
///
///
/// Aus dem echten Bestand. „Doctor Strange" war fälschlich auf ein
/// Making-of von 2022 gelaufen. Beim Korrigieren hängte die Suche
/// year=2022 an — aus genau der Zuordnung, die gerade weg sollte —
/// und lieferte nur noch Titel von 2022. Der richtige Film von 2016 konnte
/// gar nicht erscheinen. Gesucht wird deshalb mit dem Jahr aus dem
/// Dateinamen, nie mit dem aus dem Treffer.
///
[Test]
public async Task Die_Vorschlagssuche_nimmt_das_Jahr_aus_dem_Dateinamen()
{
Datei("Doctor Strange (2016).mkv");
var anbieter = new VorgegebenerAnbieter
{
// Der falsche Treffer: anderes Jahr als im Dateinamen.
Film = new MovieMetadata(980017, "Das Making of", 2022, null, null, null)
{
Language = "de-DE",
},
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(filme!.Items[0].Year, Is.EqualTo(2022), "Der falsche Treffer sitzt.");
anbieter.Gefragt.Clear();
await client.GetFromJsonAsync>(
$"/api/v1/movies/{filme.Items[0].Id}/matches");
Assert.That(anbieter.Gefragt, Does.Contain("suchFilm:Doctor Strange:2016"),
"Gesucht wird mit 2016 aus dem Dateinamen, nicht mit 2022 aus dem Treffer.");
}
/// Zwei Dateien auf denselben Film zu legen ist kein Fehler.
///
///
/// Aus dem echten Bestand. Im Zuordnungsdialog stand plötzlich „An
/// error occurred while processing your request." — englisch, ohne Grund.
/// Der gewählte Film war in derselben Mediathek schon vergeben, der
/// Eindeutigkeitsindex schlug zu, und die Ausnahme ging ungefiltert nach
/// oben.
///
/// Es ist aber gar kein Fehler: zwei Dateien, die denselben Film meinen,
/// gehören zusammengelegt — genau das tut der Scan am Ende jedes Laufs.
///
[Test]
public async Task Zwei_Dateien_auf_denselben_Film_werden_zusammengelegt()
{
Datei("Breaking Dawn Teil 1 (2011).mkv");
Datei("Breaking Dawn (2011).mkv");
// Beim Scan findet keiner etwas — so bleiben es zwei Einträge.
var anbieter = new VorgegebenerAnbieter { Film = null };
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(filme!.Total, Is.EqualTo(2), "Zwei Dateien, zwei Einträge ohne Zuordnung.");
anbieter.Film = new MovieMetadata(50619, "Breaking Dawn – Teil 1", 2011, null, null, null)
{
Language = "de-DE",
};
var erste = await client.PutAsJsonAsync(
$"/api/v1/movies/{filme.Items[0].Id}/match", new ApplyMatchRequest(50619));
Assert.That(erste.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var zweite = await client.PutAsJsonAsync(
$"/api/v1/movies/{filme.Items[1].Id}/match", new ApplyMatchRequest(50619));
Assert.That(zweite.StatusCode, Is.EqualTo(HttpStatusCode.OK),
"Die zweite Zuordnung darf nicht scheitern.");
var danach = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(danach!.Total, Is.EqualTo(1), "Ein Film.");
Assert.That(danach.Items[0].Files, Has.Count.EqualTo(2), "Mit beiden Dateien.");
}
[Test]
public async Task Ein_Film_ohne_Treffer_behaelt_seinen_Dateinamen()
{
Datei("Urlaub Kreta 2019.mkv");
using var server = MitAnbieter(new VorgegebenerAnbieter { Film = null });
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(filme!.Total, Is.EqualTo(1));
Assert.That(filme.Items[0].Title, Is.EqualTo("Urlaub Kreta"));
}
/// Ohne Schlüssel läuft der Scan trotzdem durch.
///
///
/// Das ist der Normalfall beim allerersten Start: Bibliothek angelegt,
/// Schlüssel noch nicht. Die Mediathek muss dann aus Dateinamen bestehen
/// und nicht leer sein.
///
[Test]
public async Task Ohne_Schluessel_bleibt_es_bei_den_Dateinamen()
{
Datei("The Matrix (1999).mkv");
var anbieter = new VorgegebenerAnbieter { Konfiguriert = false };
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
var stand = await Scannen(client, bibliothek);
Assert.That(stand.Metadata!.Skipped, Is.True);
Assert.That(anbieter.Gefragt, Is.Empty);
Assert.That((await client.GetFromJsonAsync>("/api/v1/movies"))!.Total, Is.EqualTo(1));
}
/// Ein falscher Schlüssel steht im Scanbericht, nicht nur im Protokoll.
[Test]
public async Task Ein_zurueckgewiesener_Schluessel_erscheint_im_Bericht()
{
Datei("The Matrix (1999).mkv");
var anbieter = new VorgegebenerAnbieter
{
Wirft = new MetadataCredentialsException("TMDB weist den hinterlegten Schlüssel zurück."),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
var stand = await Scannen(client, bibliothek);
Assert.That(stand.Metadata!.Error, Does.Contain("Schlüssel"));
// Die Datei ist trotzdem da — das Nachschlagen scheiterte, nicht der Scan.
Assert.That((await client.GetFromJsonAsync>("/api/v1/movies"))!.Total, Is.EqualTo(1));
}
[Test]
public async Task Ein_zweiter_Lauf_fragt_nicht_noch_einmal()
{
Datei("The Matrix (1999).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(603, "Matrix", 1999, "Neo.", null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var anzahlNachErstem = anbieter.Gefragt.Count;
await Scannen(client, bibliothek);
Assert.That(anbieter.Gefragt, Has.Count.EqualTo(anzahlNachErstem),
"Ein zugeordneter Titel wird nicht erneut nachgeschlagen");
}
[Test]
public async Task Serien_und_Folgen_werden_nachgeschlagen()
{
Datei("Dark/Dark - S01E01.mkv");
var anbieter = new VorgegebenerAnbieter
{
Serie = new SeriesMetadata(70523, "Dark", 2017, "Vier Familien.", null, null),
Folge = new EpisodeMetadata("Geheimnisse", "Ein Junge verschwindet.", null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Shows");
var stand = await Scannen(client, bibliothek);
Assert.That(stand.Metadata!.SeriesMatched, Is.EqualTo(1));
Assert.That(stand.Metadata.EpisodesMatched, Is.EqualTo(1));
Assert.That(anbieter.Gefragt, Does.Contain("folge:70523:1:1"));
var serien = await client.GetFromJsonAsync>("/api/v1/series");
Assert.That(serien!.Items[0].Overview, Is.EqualTo("Vier Familien."));
var folgen = await client.GetFromJsonAsync>(
$"/api/v1/series/{serien.Items[0].Id}/episodes");
Assert.That(folgen!.Items[0].Title, Is.EqualTo("Geheimnisse"));
Assert.That(folgen.Items[0].Overview, Is.EqualTo("Ein Junge verschwindet."));
}
/// Folgen einer Serie ohne Zuordnung werden gar nicht erst gefragt.
[Test]
public async Task Ohne_zugeordnete_Serie_werden_keine_Folgen_gefragt()
{
Datei("Unbekannt/Unbekannt - S01E01.mkv");
var anbieter = new VorgegebenerAnbieter { Serie = null };
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Shows");
await Scannen(client, bibliothek);
Assert.That(anbieter.Gefragt.Where(g => g.StartsWith("folge:")), Is.Empty);
}
}
public class MetadataSettingsTests
{
/// Der Schlüssel darf nie zurückkommen.
///
///
/// Er gäbe jedem Angemeldeten das TMDB-Kontingent des Betreibers in die
/// Hand. Zum Anzeigen genügt die Auskunft, dass einer da ist.
///
[Test]
public async Task Der_Schluessel_kommt_nicht_zurueck()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var client = server.AlsAngemeldeter(admin.Token);
await client.PutAsJsonAsync("/api/v1/settings/metadata",
new MetadataSettingsRequest("geheimer-schluessel", "de-DE"));
var antwort = await client.GetAsync("/api/v1/settings/metadata");
var roh = await antwort.Content.ReadAsStringAsync();
Assert.That(roh, Does.Not.Contain("geheimer-schluessel"));
var stand = await antwort.Content.ReadFromJsonAsync();
Assert.That(stand!.HasApiKey, Is.True);
Assert.That(stand.Language, Is.EqualTo("de-DE"));
}
/// Ohne eigenen Schlüssel greift der mitgelieferte.
///
///
/// UsingProjectKey ist der Unterschied, den die Oberfläche zeigen
/// muss: „es läuft, aber über das gemeinsame Kontingent" ist etwas anderes
/// als „du hast einen eigenen hinterlegt".
///
[Test]
public async Task Ohne_eigenen_Schluessel_greift_der_mitgelieferte()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var stand = await server.AlsAngemeldeter(admin.Token)
.GetFromJsonAsync("/api/v1/settings/metadata");
var mitgeliefert = Flaro.Metadata.TmdbSettings.ProjectApiKey.Length > 0;
Assert.That(stand!.HasApiKey, Is.EqualTo(mitgeliefert));
Assert.That(stand.UsingProjectKey, Is.EqualTo(mitgeliefert));
Assert.That(stand.Language, Is.EqualTo("en-US"));
}
[Test]
public async Task Ein_eigener_Schluessel_loest_den_mitgelieferten_ab()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var client = server.AlsAngemeldeter(admin.Token);
await client.PutAsJsonAsync("/api/v1/settings/metadata",
new MetadataSettingsRequest("mein-eigener-schluessel", "de-DE"));
var stand = await client.GetFromJsonAsync("/api/v1/settings/metadata");
Assert.That(stand!.HasApiKey, Is.True);
Assert.That(stand.UsingProjectKey, Is.False);
}
/// TMDB verlangt den Hinweis — und zwar ohne Anmeldung abrufbar.
[Test]
public async Task Der_Pflichthinweis_zu_TMDB_steht_in_der_Systemauskunft()
{
using var server = new FlaroServer();
var info = await server.Client.GetFromJsonAsync("/api/v1/system/info");
var tmdb = info!.Attributions.Single();
Assert.That(tmdb.Text, Does.Contain("TMDB").And.Contain("not endorsed"));
Assert.That(tmdb.LogoUrl, Does.StartWith("https://"));
}
[TestCase("deutsch")]
[TestCase("de_DE")]
[TestCase("DE")]
[TestCase("de-de")]
public async Task Eine_unsinnige_Sprachkennung_wird_abgelehnt(string sprache)
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var antwort = await server.AlsAngemeldeter(admin.Token)
.PutAsJsonAsync("/api/v1/settings/metadata", new MetadataSettingsRequest(null, sprache));
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
public async Task Ein_gewoehnlicher_Benutzer_sieht_die_Einstellungen_nicht()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
await server.AlsAngemeldeter(admin.Token).PostAsJsonAsync("/api/v1/users",
new CreateUserRequest("gast", "auch-ein-langes-passwort", false));
var gast = await server.SignIn("gast", "auch-ein-langes-passwort", "iPad", "g-1");
var antwort = await server.AlsAngemeldeter(gast.Token).GetAsync("/api/v1/settings/metadata");
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
}
}
public class ImageEndpointTests
{
[Test]
public async Task Bilder_gibt_es_nur_fuer_Angemeldete()
{
using var server = new FlaroServer();
await server.SetUpAndSignIn();
// **Ohne Gedächtnis, weil der Client nach der Anmeldung ein Cookie
// hält.** Sonst prüfte dieser Test nicht den Unangemeldeten, sondern
// den Angemeldeten — und bestünde nie wieder.
var antwort = await server.OhneGedaechtnis().GetAsync($"/api/v1/images/{new string('a', 64)}");
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
/// Der Weg, der sonst die Datenbank herausgäbe.
[TestCase("..%2F..%2Fconfig%2Fflaro.db")]
[TestCase("nichtsechzigvierzeichen")]
[TestCase("ZZZZ567890123456789012345678901234567890123456789012345678901234")]
public async Task Ein_Schluessel_der_keiner_ist_fuehrt_ins_Leere(string versuch)
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var antwort = await server.AlsAngemeldeter(admin.Token).GetAsync($"/api/v1/images/{versuch}");
Assert.That(antwort.StatusCode, Is.AnyOf(HttpStatusCode.NotFound, HttpStatusCode.BadRequest));
}
}