Files
ScarriffleandClaude Opus 5 18db89399d Ein leeres Jahrfeld ist eine Angabe, kein Versehen
Wer bei der Zuordnung kein Jahr einträgt, sagt damit, dass er keines
weiss. Der Server reichte trotzdem eines nach — das aus dem Dateinamen —,
und das leere Feld hatte überhaupt keine Wirkung. Gerade bei einem falsch
benannten Film ist der Dateiname aber die unzuverlässigste Quelle, die es
gibt: „Plan B (2010)" heisst bei TMDB „DivertiMento – Plan B" und ist von
2011. Mit dem nachgereichten 2010 kam nie ein Treffer, und es gab keinen
Weg daran vorbei.

Ohne Angabe wird jetzt ohne Jahr gesucht. Der Platzhalter sagt es auch.

Auf der Detailseite stand im Feld bisher das Jahr der bestehenden
Zuordnung — also genau der, die gerade korrigiert werden soll. „Doctor
Strange" war auf ein Making-of von 2022 gelaufen; mit dessen Jahr konnte
der richtige Film von 2016 gar nicht erscheinen. Das Feld bleibt jetzt
leer, und leer heisst überall dasselbe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 16:02:29 +02:00

1228 lines
50 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
using Flaro.Core.Metadata;
using Microsoft.Extensions.DependencyInjection;
namespace Flaro.Api.Tests;
/// <summary>Ein Anbieter, der antwortet, was der Test vorgibt.</summary>
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<string> Gefragt { get; } = [];
public Task<bool> IsConfiguredAsync(CancellationToken ct = default) => Task.FromResult(Konfiguriert);
public Task<MovieMetadata?> 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<SeriesMetadata?> 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<EpisodeMetadata?> 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<MovieMetadata?> GetMovieAsync(int id, CancellationToken ct = default)
{
Gefragt.Add($"filmId:{id}");
if (Wirft is not null) throw Wirft;
return Task.FromResult(Film);
}
public Task<SeriesMetadata?> GetSeriesAsync(int id, CancellationToken ct = default)
{
Gefragt.Add($"serieId:{id}");
if (Wirft is not null) throw Wirft;
return Task.FromResult(Serie);
}
public Task<IReadOnlyList<MovieMetadata>> 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<IReadOnlyList<MovieMetadata>>(Film is null ? [] : [Film]);
}
public Task<IReadOnlyList<SeriesMetadata>> SearchSeriesAsync(string t, int? j, CancellationToken ct = default)
{
Gefragt.Add($"suchSerie:{t}");
return Task.FromResult<IReadOnlyList<SeriesMetadata>>(Serie is null ? [] : [Serie]);
}
}
/// <summary>Eine Bildablage, die nichts herunterlädt.</summary>
public sealed class VorgegebeneBilder : IImageStore
{
public List<string> Geholt { get; } = [];
public Task<string?> StoreAsync(RemoteImage bild, CancellationToken ct = default)
{
Geholt.Add(bild.Url);
return Task.FromResult<string?>(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<IMetadataProvider>(_ => anbieter);
dienste.AddScoped<IImageStore>(_ => 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<LibraryResponse>())!.Id);
}
private static async Task<ScanStatus> 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<ScanStatus>($"/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<Page<MovieResponse>>("/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" }));
}
/// <summary>Zwei Namen, ein Film — das darf den Lauf nicht abbrechen.</summary>
///
/// <remarks>
/// <para>
/// <b>Der Fehler, den der Eindeutigkeitsindex selbst verursacht hat.</b>
/// 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.
/// </para>
/// <para>
/// Hier antwortet der Anbieter auf jede Anfrage mit demselben Film — also
/// genau die Lage, die vorher den Lauf zerriss.
/// </para>
/// </remarks>
[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<Page<MovieResponse>>("/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.");
}
/// <summary>Dasselbe für Serien.</summary>
[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<Page<SeriesResponse>>("/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.");
}
/// <summary>Was die Quelle nicht kennt, bleibt trotzdem in der Mediathek.</summary>
/// <summary>„Metadaten auffrischen" muss den Titel wirklich fortschreiben.</summary>
///
/// <remarks>
/// <b>Der teuerste Fehler der ganzen Sprachgeschichte.</b> 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".
/// </remarks>
[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<Page<MovieResponse>>("/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<ScanStatus>($"/api/v1/libraries/{bibliothek}/scan");
if (stand!.State == "finished") break;
await Task.Delay(50);
}
var nachher = await client.GetFromJsonAsync<Page<MovieResponse>>("/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."));
}
/// <summary>Was von Hand benannt wurde, überlebt das Auffrischen.</summary>
[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<Page<MovieResponse>>("/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<ScanStatus>($"/api/v1/libraries/{bibliothek}/scan");
if (stand!.State == "finished") break;
await Task.Delay(50);
}
var nachher = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
Assert.That(nachher!.Items[0].Title, Is.EqualTo("Von Hand"));
}
/// <summary>Auffrischen nimmt eine Auswahl entgegen — und geht auch ohne.</summary>
///
/// <remarks>
/// <b>Gemessen, nachdem es einmal brach.</b> Als die Auswahl als
/// <c>RefreshRequest?</c> 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.
/// </remarks>
[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<Page<MovieResponse>>("/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<ScanStatus> Fertig(HttpClient client, Guid bibliothek)
{
for (var i = 0; i < 200; i++)
{
var stand = await client.GetFromJsonAsync<ScanStatus>($"/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 ---------- */
/// <summary>Zwei Filme verschiedener Jahre werden nicht zusammengelegt.</summary>
///
/// <remarks>
/// <para>
/// <b>Aus dem echten Bestand, und es kostete drei Filme.</b> „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 <i>beide</i> 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
[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<Page<MovieResponse>>("/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<List<OhneZuordnung>>(
$"/api/v1/libraries/{bibliothek}/unmatched");
Assert.That(ohne, Has.Count.EqualTo(1),
"der zweite bleibt ohne Zuordnung — und steht dafür in der Liste");
}
/// <summary>Derselbe Film in zwei Ordnern wird weiterhin zusammengelegt.</summary>
///
/// <remarks>
/// <b>Die Gegenprobe, und sie ist wichtiger als sie aussieht.</b> „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.
/// </remarks>
[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<Page<MovieResponse>>("/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");
});
}
/// <summary>Ohne Jahr im Namen wird weiter zusammengelegt.</summary>
///
/// <remarks>
/// <b>Eine Lücke ist keine Aussage.</b> 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.
/// </remarks>
[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<Page<MovieResponse>>("/api/v1/movies");
Assert.That(filme!.Items, Has.Count.EqualTo(1));
}
/// <summary>Titel ohne Zuordnung stehen in einer Liste — mit Dateinamen.</summary>
[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<List<OhneZuordnung>>(
$"/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");
});
}
/// <summary>Eine Datei lässt sich aus ihrem Titel herauslösen.</summary>
///
/// <remarks>
/// <b>Weil die Riegel nur Neues verhindern.</b> Was schon zusammengelegt
/// ist, bleibt es — „Hanni und Nanni" und „Hanni und Nanni 3" hängen dann
/// an einem Eintrag, und einer der beiden Filme ist nicht mehr zu sehen.
/// Der neue Eintrag bekommt den Titel aus dem Dateinamen und keine
/// Kennung; eine zu raten hiesse, denselben Fehler zu wiederholen.
/// </remarks>
[Test]
public async Task Eine_Datei_laesst_sich_aus_ihrem_Titel_herausloesen()
{
Datei("Hanni und Nanni/Hanni und Nanni.mp4");
Datei("Hanni und Nanni/Hanni und Nanni 3.mp4");
// Die Quelle liefert für beides denselben Film — der Fall aus dem Betrieb.
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(225313, "Hanni & Nanni 3", 2013, null, null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var mehrfach = await client.GetFromJsonAsync<Mehrfachbericht>(
$"/api/v1/libraries/{bibliothek}/versions");
Assert.That(mehrfach!.Gesamt, Is.EqualTo(1), "zwei Dateien an einem Titel");
var dateien = mehrfach.Titel[0].Dateien;
Assert.That(dateien, Has.Count.EqualTo(2));
var raus = dateien.First(f => f.Name.Contains(" 3"));
var antwort = await client.PostAsync($"/api/v1/files/{raus.Id}/split", null);
antwort.EnsureSuccessStatusCode();
var neu = (await antwort.Content.ReadFromJsonAsync<MatchState>())!;
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
Assert.Multiple(() =>
{
Assert.That(filme!.Items, Has.Count.EqualTo(2), "jetzt zwei Filme");
Assert.That(neu.Title, Is.EqualTo("Hanni und Nanni 3"), "der Titel kommt aus dem Dateinamen");
Assert.That(neu.TmdbId, Is.Null, "und ohne Kennung — geraten wird nicht");
Assert.That(filme.Items.Select(m => m.Files.Count), Is.All.EqualTo(1));
});
}
/// <summary>Die saisonale Reihe findet, was das Stichwort trägt.</summary>
///
/// <remarks>
/// <b>Nach Stichwörtern, nicht nach Genres.</b> „Stirb langsam" ist kein
/// Familienfilm und trägt trotzdem „christmas" — eine Weihnachtsreihe
/// nach Genres enthielte jeden Zeichentrickfilm und diesen Film nicht.
/// Genau das prüft dieser Test: der Actionfilm mit dem Stichwort ist
/// drin, der Familienfilm ohne es nicht.
/// </remarks>
[Test]
public async Task Die_saisonale_Reihe_geht_nach_Stichwort_und_nicht_nach_Genre()
{
Datei("Stirb langsam (1988)/Stirb langsam (1988).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(562, "Stirb langsam", 1988, null, null, null)
{
Genres = ["Action"],
Keywords = ["christmas", "terrorist"],
},
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
// Ein Anlass, der heute gilt — und einer, der nie gilt.
await client.PutAsJsonAsync("/api/v1/settings/seasonal", new[]
{
new { name = "Gerade jetzt", von = "01-01", bis = "12-31", stichworte = new[] { "christmas" }, genres = Array.Empty<string>() },
});
var reihe = await client.GetFromJsonAsync<Saisonreihe>("/api/v1/home/seasonal");
Assert.Multiple(() =>
{
Assert.That(reihe!.Occasion, Is.EqualTo("Gerade jetzt"));
Assert.That(reihe.Items.Items.Select(f => f.Title), Is.EquivalentTo(new[] { "Stirb langsam" }));
});
}
/// <summary>Was weder Stichwort noch Genre trifft, steht nicht in der Reihe.</summary>
///
/// <remarks>
/// <b>Gemeldet vom Betrieb:</b> zu Halloween standen „Die Schöne und das
/// Biest" und ein Feenfilm in der Reihe. Nachgesehen bei TMDB: keiner
/// von beiden trägt eines der Halloween-Stichwörter, und Horror sind sie
/// auch nicht.
/// </remarks>
[Test]
public async Task Ein_Film_ohne_Treffer_steht_nicht_in_der_Reihe()
{
Datei("Die Schoene und das Biest (1991)/Die Schoene und das Biest (1991).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(10020, "Die Schöne und das Biest", 1991, null, null, null)
{
Genres = ["Familie", "Fantasy"],
Keywords = ["princess", "castle", "rose"],
},
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
await client.PutAsJsonAsync("/api/v1/settings/seasonal", new[]
{
new
{
name = "Zu Halloween", von = "01-01", bis = "12-31",
stichworte = new[] { "halloween", "zombie" },
genres = new[] { "Horror" },
},
});
var reihe = await client.GetFromJsonAsync<Saisonreihe>("/api/v1/home/seasonal");
Assert.That(reihe!.Items.Items, Is.Empty,
"weder Stichwort noch Genre passen — der Film gehört nicht in die Reihe");
}
/// <summary>Gilt heute kein Anlass, kommt keine Reihe.</summary>
///
/// <remarks>
/// Und zwar ohne Namen: der Client zeigt dann gar keine Reihe, statt
/// einer mit Überschrift und nichts darunter.
/// </remarks>
[Test]
public async Task Ohne_geltenden_Anlass_bleibt_die_Reihe_leer()
{
Datei("Stirb langsam (1988)/Stirb langsam (1988).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(562, "Stirb langsam", 1988, null, null, null)
{
Keywords = ["christmas"],
},
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
// Ein einzelner Tag, der heute mit grosser Wahrscheinlichkeit nicht ist —
// und wenn doch, prüft der zweite Anlass daneben dasselbe.
var heute = DateTimeOffset.UtcNow;
var fern = heute.AddDays(100);
await client.PutAsJsonAsync("/api/v1/settings/seasonal", new[]
{
new
{
name = "Weit weg", von = $"{fern:MM-dd}", bis = $"{fern:MM-dd}",
stichworte = new[] { "christmas" }, genres = Array.Empty<string>(),
},
});
var reihe = await client.GetFromJsonAsync<Saisonreihe>("/api/v1/home/seasonal");
Assert.Multiple(() =>
{
Assert.That(reihe!.Occasion, Is.Null);
Assert.That(reihe.Items.Items, Is.Empty);
});
}
/// <summary>Eine Videomediathek nimmt den Dateinamen und fragt niemanden.</summary>
///
/// <remarks>
/// <para>
/// <b>Für alles, was in keinem Katalog steht.</b> Urlaubsaufnahmen,
/// Mitschnitte, Vorträge. Jedes der Verfahren, die bei Filmen richtig
/// sind, wäre hier falsch: „Sommer 2019 Teil 2" ist kein Film von 2019,
/// und zwei Aufnahmen desselben Tages sind nicht zwei Fassungen
/// derselben Sache.
/// </para>
/// <para>
/// <b>Der Unterordner wird zur Kategorie</b> — damit greifen Filter und
/// Kategorienansicht, ohne dass es dort etwas Neues gibt.
/// </para>
/// </remarks>
[Test]
public async Task Eine_Videomediathek_nimmt_den_Dateinamen_als_Titel()
{
Datei("Ferien 2019/Ausflug zum See.mp4");
Datei("Ferien 2019/Abend am Feuer.mp4");
Datei("Vortraege/Wie Dateisysteme altern.mp4");
Datei("Direkt daneben.mp4");
// Die Quelle würde antworten — gefragt werden darf sie trotzdem nicht.
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(999, "Irgendein fremder Film", 1999, null, null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Videos");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
var titel = filme!.Items.Select(f => f.Title).ToList();
Assert.Multiple(() =>
{
Assert.That(titel, Is.EquivalentTo(new[]
{
"Ausflug zum See", "Abend am Feuer", "Wie Dateisysteme altern", "Direkt daneben",
}));
Assert.That(filme.Items.All(f => f.Year is null), Is.True,
"der Ordnername Ferien 2019 ist kein Erscheinungsjahr");
Assert.That(titel, Has.No.Member("Irgendein fremder Film"),
"die Metadatenquelle wird hier nicht gefragt");
});
var kategorien = await client.GetFromJsonAsync<List<string>>(
$"/api/v1/libraries/{bibliothek}/genres");
Assert.That(kategorien, Is.EquivalentTo(new[] { "Ferien 2019", "Vortraege" }),
"die Unterordner werden zu Kategorien; was direkt in der Wurzel liegt, bekommt keine");
}
/// <summary>Zwei gleichnamige Dateien in zwei Ordnern sind zwei Einträge.</summary>
///
/// <remarks>
/// Bei Filmen sucht der Scanner einen vorhandenen Eintrag und hängt die
/// Datei dort an — so finden zwei Fassungen zusammen. Hier wäre das
/// falsch: zwei „Ausflug.mp4" in zwei Ordnern sind zwei Ausflüge.
/// </remarks>
[Test]
public async Task Gleichnamige_Videos_werden_nicht_zusammengelegt()
{
Datei("Mai/Ausflug.mp4");
Datei("Juni/Ausflug.mp4");
using var server = MitAnbieter(new VorgegebenerAnbieter());
var (client, bibliothek) = await Einrichten(server, "Videos");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
Assert.That(filme!.Items, Has.Count.EqualTo(2),
"zwei Dateien gleichen Namens in zwei Ordnern sind zwei Aufnahmen");
}
/// <summary>Eine abgehakte Gruppe verlässt die Liste — und kommt zurück, wenn eine Datei dazukommt.</summary>
///
/// <remarks>
/// <para>
/// <b>Der Anlass.</b> Zwei Dateien an einem Film sind meist zwei
/// Fassungen und völlig in Ordnung. In der Liste standen sie trotzdem für
/// immer: sie rechnet jedes Mal frisch, und es gab keinen Zustand. Wer
/// absichtlich zwei Fassungen hält, konnte die Gruppe nicht loswerden —
/// und unter den gewollten ging das Ungewollte unter.
/// </para>
/// <para>
/// <b>Abgehakt wird eine Dateizahl, kein Zeitpunkt.</b> Kommt eine dritte
/// Fassung dazu, stimmt die Zahl nicht mehr und die Gruppe meldet sich
/// von selbst zurück. Genau das prüft der zweite Teil.
/// </para>
/// </remarks>
[Test]
public async Task Eine_abgehakte_Fassungsgruppe_meldet_sich_bei_einer_neuen_Datei_zurueck()
{
Datei("Hanni und Nanni/Hanni und Nanni.mp4");
Datei("Hanni und Nanni/Hanni und Nanni 3.mp4");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(225313, "Hanni & Nanni 3", 2013, null, null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var vorher = await client.GetFromJsonAsync<Mehrfachbericht>(
$"/api/v1/libraries/{bibliothek}/versions");
Assert.That(vorher!.Gesamt, Is.EqualTo(1), "zwei Dateien an einem Titel");
var film = vorher.Titel[0].Id;
(await client.PostAsJsonAsync($"/api/v1/libraries/{bibliothek}/versions/acknowledge",
new FassungenRequest([film]))).EnsureSuccessStatusCode();
var offen = await client.GetFromJsonAsync<Mehrfachbericht>(
$"/api/v1/libraries/{bibliothek}/versions");
var abgehakt = await client.GetFromJsonAsync<Mehrfachbericht>(
$"/api/v1/libraries/{bibliothek}/versions?ignorierte=true");
Assert.Multiple(() =>
{
Assert.That(offen!.Gesamt, Is.Zero, "abgehakt heisst: nicht mehr in der offenen Liste");
Assert.That(abgehakt!.Gesamt, Is.EqualTo(1), "aber nachsehen muss man können");
});
// Eine dritte Fassung dazu — jetzt stimmt die abgehakte Zahl nicht mehr.
Datei("Hanni und Nanni/Hanni und Nanni 2.mp4");
await Scannen(client, bibliothek);
var wieder = await client.GetFromJsonAsync<Mehrfachbericht>(
$"/api/v1/libraries/{bibliothek}/versions");
Assert.That(wieder!.Gesamt, Is.EqualTo(1),
"eine neue Datei macht die Gruppe wieder zu einer Frage");
}
/// <summary>Zurückholen macht das Abhaken rückgängig.</summary>
[Test]
public async Task Eine_abgehakte_Gruppe_laesst_sich_zurueckholen()
{
Datei("Hanni und Nanni/Hanni und Nanni.mp4");
Datei("Hanni und Nanni/Hanni und Nanni 3.mp4");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(225313, "Hanni & Nanni 3", 2013, null, null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var film = (await client.GetFromJsonAsync<Mehrfachbericht>(
$"/api/v1/libraries/{bibliothek}/versions"))!.Titel[0].Id;
await client.PostAsJsonAsync($"/api/v1/libraries/{bibliothek}/versions/acknowledge",
new FassungenRequest([film]));
await client.PostAsJsonAsync($"/api/v1/libraries/{bibliothek}/versions/acknowledge",
new FassungenRequest([film], Ignorieren: false));
var offen = await client.GetFromJsonAsync<Mehrfachbericht>(
$"/api/v1/libraries/{bibliothek}/versions");
Assert.That(offen!.Gesamt, Is.EqualTo(1));
}
/// <summary>Eine einzelne Datei lässt sich nicht herauslösen.</summary>
[Test]
public async Task Eine_einzelne_Datei_laesst_sich_nicht_herausloesen()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
var anbieter = new VorgegebenerAnbieter
{
Film = new MovieMetadata(603, "Matrix", 1999, null, null, null),
};
using var server = MitAnbieter(anbieter);
var (client, bibliothek) = await Einrichten(server, "Movies");
await Scannen(client, bibliothek);
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
var datei = filme!.Items[0].Files.Single();
var antwort = await client.PostAsync($"/api/v1/files/{datei.Id}/split", null);
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
}
/// <summary>Eine falsche Zuordnung darf ihre eigene Korrektur nicht verhindern.</summary>
///
/// <remarks>
/// <para>
/// <b>Aus dem echten Bestand.</b> „Doctor Strange" war fälschlich auf ein
/// Making-of von 2022 gelaufen. Beim Korrigieren hängte die Suche
/// <c>year=2022</c> 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.
/// </para>
/// <para>
/// <b>Danach stand hier das Jahr aus dem Dateinamen, und auch das war
/// falsch.</b> Ein leeres Jahrfeld heisst „ich weiss es nicht" — und
/// gerade bei einem falsch benannten Film ist der Dateiname die
/// unzuverlässigste Quelle, die es gibt. „Plan B (2010)" heisst bei TMDB
/// „DivertiMento – Plan B" und ist von 2011; mit dem nachgereichten 2010
/// kam nie ein Treffer, und das leere Feld hatte keine Wirkung. Ohne
/// Angabe wird jetzt ohne Jahr gesucht.
/// </para>
/// </remarks>
[Test]
public async Task Die_Vorschlagssuche_nimmt_ohne_Angabe_gar_kein_Jahr()
{
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<Page<MovieResponse>>("/api/v1/movies");
Assert.That(filme!.Items[0].Year, Is.EqualTo(2022), "Der falsche Treffer sitzt.");
anbieter.Gefragt.Clear();
await client.GetFromJsonAsync<List<MatchCandidate>>(
$"/api/v1/movies/{filme.Items[0].Id}/matches");
Assert.That(anbieter.Gefragt, Does.Contain("suchFilm:Doctor Strange:"),
"Ohne Angabe wird ohne Jahr gesucht — weder mit 2022 aus dem Treffer "
+ "noch mit 2016 aus dem Dateinamen.");
}
/// <summary>Zwei Dateien auf denselben Film zu legen ist kein Fehler.</summary>
///
/// <remarks>
/// <b>Aus dem echten Bestand.</b> 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.
/// </remarks>
[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<Page<MovieResponse>>("/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<Page<MovieResponse>>("/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<Page<MovieResponse>>("/api/v1/movies");
Assert.That(filme!.Total, Is.EqualTo(1));
Assert.That(filme.Items[0].Title, Is.EqualTo("Urlaub Kreta"));
}
/// <summary>Ohne Schlüssel läuft der Scan trotzdem durch.</summary>
///
/// <remarks>
/// Das ist der Normalfall beim allerersten Start: Bibliothek angelegt,
/// Schlüssel noch nicht. Die Mediathek muss dann aus Dateinamen bestehen
/// und nicht leer sein.
/// </remarks>
[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<Page<MovieResponse>>("/api/v1/movies"))!.Total, Is.EqualTo(1));
}
/// <summary>Ein falscher Schlüssel steht im Scanbericht, nicht nur im Protokoll.</summary>
[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<Page<MovieResponse>>("/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<Page<SeriesResponse>>("/api/v1/series");
Assert.That(serien!.Items[0].Overview, Is.EqualTo("Vier Familien."));
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/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."));
}
/// <summary>Folgen einer Serie ohne Zuordnung werden gar nicht erst gefragt.</summary>
[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
{
/// <summary>Der Schlüssel darf nie zurückkommen.</summary>
///
/// <remarks>
/// Er gäbe jedem Angemeldeten das TMDB-Kontingent des Betreibers in die
/// Hand. Zum Anzeigen genügt die Auskunft, dass einer da ist.
/// </remarks>
[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<MetadataSettingsResponse>();
Assert.That(stand!.HasApiKey, Is.True);
Assert.That(stand.Language, Is.EqualTo("de-DE"));
}
/// <summary>Ohne eigenen Schlüssel greift der mitgelieferte.</summary>
///
/// <remarks>
/// <c>UsingProjectKey</c> 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".
/// </remarks>
[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<MetadataSettingsResponse>("/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<MetadataSettingsResponse>("/api/v1/settings/metadata");
Assert.That(stand!.HasApiKey, Is.True);
Assert.That(stand.UsingProjectKey, Is.False);
}
/// <summary>TMDB verlangt den Hinweis — und zwar ohne Anmeldung abrufbar.</summary>
[Test]
public async Task Der_Pflichthinweis_zu_TMDB_steht_in_der_Systemauskunft()
{
using var server = new FlaroServer();
var info = await server.Client.GetFromJsonAsync<SystemInfo>("/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));
}
/// <summary>Der Weg, der sonst die Datenbank herausgäbe.</summary>
[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));
}
}