„Doctor Strange" war fälschlich auf ein Making-of von 2022 gelaufen. Beim Korrigieren hängte die Vorschlagssuche `year=2022` an — aus genau der Zuordnung, die weg sollte — und lieferte nur noch Titel von 2022. Der richtige Film von 2016 konnte gar nicht erscheinen, egal wie oft man suchte. An TMDB nachgemessen: query='Doctor Strange' ohne Jahr → 2016 Doctor Strange, 2022 Multiverse … query='Doctor Strange' mit year=2022 → genau die drei Treffer aus der Meldung Gesucht wird jetzt mit dem Jahr aus dem Dateinamen, nie mit dem aus dem Treffer. Die Oberfläche trägt ebenfalls keines mehr vor: das angezeigte stammt aus derselben falschen Zuordnung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
604 lines
24 KiB
C#
604 lines
24 KiB
C#
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>Eine falsche Zuordnung darf ihre eigene Korrektur nicht verhindern.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <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. Gesucht wird deshalb mit dem Jahr aus dem
|
|
/// Dateinamen, nie mit dem aus dem Treffer.
|
|
/// </remarks>
|
|
[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<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:2016"),
|
|
"Gesucht wird mit 2016 aus dem Dateinamen, nicht mit 2022 aus dem Treffer.");
|
|
}
|
|
|
|
[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));
|
|
}
|
|
}
|