Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f98b995050
commit
a7cf6c5def
@@ -0,0 +1,118 @@
|
||||
using Flaro.Core.Auth;
|
||||
|
||||
namespace Flaro.Core.Tests;
|
||||
|
||||
public class SecretsTests
|
||||
{
|
||||
[Test]
|
||||
public void Zwei_Geheimnisse_sind_nie_gleich()
|
||||
{
|
||||
var gesehen = new HashSet<string>();
|
||||
for (var i = 0; i < 200; i++) Assert.That(gesehen.Add(Secrets.Create()), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Das_Geheimnis_ist_URL_sicher()
|
||||
{
|
||||
for (var i = 0; i < 50; i++)
|
||||
{
|
||||
var s = Secrets.Create();
|
||||
Assert.That(s, Does.Not.Contain("+").And.Not.Contain("/").And.Not.Contain("="));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Der_Fingerabdruck_ist_gleichbleibend_und_unterscheidet()
|
||||
{
|
||||
var a = Secrets.Create();
|
||||
var b = Secrets.Create();
|
||||
// Zweimal gerechnet, nicht zweimal derselbe Ausdruck: geprüft wird,
|
||||
// dass die Funktion bei gleicher Eingabe gleich antwortet.
|
||||
var ersterLauf = Secrets.Fingerprint(a);
|
||||
var zweiterLauf = Secrets.Fingerprint(a);
|
||||
Assert.That(zweiterLauf, Is.EqualTo(ersterLauf));
|
||||
Assert.That(Secrets.Fingerprint(b), Is.Not.EqualTo(ersterLauf));
|
||||
Assert.That(ersterLauf, Has.Length.EqualTo(64));
|
||||
}
|
||||
|
||||
/// <summary>Der Fingerabdruck darf das Geheimnis nicht enthalten.</summary>
|
||||
[Test]
|
||||
public void Der_Fingerabdruck_verraet_das_Geheimnis_nicht()
|
||||
{
|
||||
var s = Secrets.Create();
|
||||
Assert.That(Secrets.Fingerprint(s), Does.Not.Contain(s));
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthCodeTests
|
||||
{
|
||||
private static readonly DateTimeOffset Jetzt = new(2026, 10, 3, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static AuthCode Angelegt(DateTimeOffset? läuftAb = null) => new()
|
||||
{
|
||||
Code = AuthCode.NewCode(),
|
||||
HandleFingerprint = Secrets.Fingerprint(Secrets.Create()),
|
||||
DeviceName = "Wohnzimmer",
|
||||
DeviceId = "tv-1",
|
||||
CreatedAt = Jetzt,
|
||||
ExpiresAt = läuftAb ?? Jetzt + AuthCode.Lifetime,
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void Der_Code_hat_immer_sechs_Ziffern()
|
||||
{
|
||||
for (var i = 0; i < 500; i++)
|
||||
{
|
||||
var code = AuthCode.NewCode();
|
||||
Assert.That(code, Has.Length.EqualTo(6));
|
||||
Assert.That(code, Does.Match("^[0-9]{6}$"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Führende Nullen dürfen nicht wegfallen — „000123" ist ein gültiger Code.</summary>
|
||||
[Test]
|
||||
public void Fuehrende_Nullen_bleiben_erhalten()
|
||||
{
|
||||
var mitNull = Enumerable.Range(0, 20_000).Select(_ => AuthCode.NewCode()).Any(c => c[0] == '0');
|
||||
Assert.That(mitNull, Is.True, "In 20 000 Ziehungen kam kein Code mit führender Null vor");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ohne_Bestaetigung_gibt_es_kein_Token()
|
||||
{
|
||||
Assert.That(Angelegt().CanBeRedeemed(Jetzt), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Nach_Bestaetigung_gibt_es_ein_Token()
|
||||
{
|
||||
var code = Angelegt();
|
||||
code.ApprovedByUserId = Guid.CreateVersion7();
|
||||
Assert.That(code.CanBeRedeemed(Jetzt), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>Einmal abgeholt ist abgeholt.</summary>
|
||||
[Test]
|
||||
public void Ein_verbrauchter_Code_gibt_kein_zweites_Token()
|
||||
{
|
||||
var code = Angelegt();
|
||||
code.ApprovedByUserId = Guid.CreateVersion7();
|
||||
code.ConsumedAt = Jetzt;
|
||||
Assert.That(code.CanBeRedeemed(Jetzt), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ein_abgelaufener_Code_gibt_kein_Token_auch_wenn_bestaetigt()
|
||||
{
|
||||
var code = Angelegt(läuftAb: Jetzt);
|
||||
code.ApprovedByUserId = Guid.CreateVersion7();
|
||||
Assert.That(code.IsExpired(Jetzt), Is.True);
|
||||
Assert.That(code.CanBeRedeemed(Jetzt), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Die_Gueltigkeit_ist_kurz()
|
||||
{
|
||||
Assert.That(AuthCode.Lifetime, Is.LessThanOrEqualTo(TimeSpan.FromMinutes(10)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using Flaro.Core.Delivery;
|
||||
using Flaro.Core.Library;
|
||||
|
||||
namespace Flaro.Core.Tests;
|
||||
|
||||
public class DeliveryDeciderTests
|
||||
{
|
||||
private static ProbedStream Bild(string codec = "h264") =>
|
||||
new(0, MediaStreamKind.Video, codec, null, null, true, false, null, 1920, 1080);
|
||||
|
||||
private static ProbedStream Ton(int index, string codec = "aac", string? sprache = "deu", string? name = null) =>
|
||||
new(index, MediaStreamKind.Audio, codec, sprache, name, index == 1, false, 6, null, null);
|
||||
|
||||
private static ProbedStream Text(int index, string codec = "subrip") =>
|
||||
new(index, MediaStreamKind.Subtitle, codec, "deu", null, false, false, null, null, null);
|
||||
|
||||
[Test]
|
||||
public void Eine_passende_MP4_geht_unveraendert_hinaus()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload("mp4", [Bild(), Ton(1)]);
|
||||
|
||||
Assert.That(plan.Method, Is.EqualTo(DeliveryMethod.DirectFile));
|
||||
Assert.That(plan.NeedsWork, Is.False);
|
||||
Assert.That(plan.Reasons, Is.EqualTo(new[] { DeliveryReason.NativelySupported }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Eine_Matroska_muss_umgepackt_werden_auch_wenn_alles_darin_passt()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload("mkv", [Bild(), Ton(1)]);
|
||||
|
||||
Assert.That(plan.Method, Is.EqualTo(DeliveryMethod.Remux));
|
||||
Assert.That(plan.Reasons, Does.Contain(DeliveryReason.ContainerUnsupported));
|
||||
// Das Bild bleibt unberührt, und kein Ton muss gerechnet werden.
|
||||
Assert.That(plan.AudioToEncode, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Der Fall, um den es im ganzen Projekt geht.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Zwei Tonspuren, und beide kommen mit.</b> Der Plan hat gar kein Feld
|
||||
/// „welche Tonspur" — und das ist der Unterschied zu Jellyfin, dessen
|
||||
/// Transcode-Endpunkt je Sitzung genau eine abbildet. Wer hier eine
|
||||
/// auswählen wollte, fände keine Stelle dafür.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Alle_Tonspuren_bleiben_erhalten()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload("mkv",
|
||||
[Bild(), Ton(1, name: "Deutsch"), Ton(2, name: "Audiodeskription"), Ton(3, sprache: "eng")]);
|
||||
|
||||
Assert.That(plan.Method, Is.EqualTo(DeliveryMethod.Remux));
|
||||
Assert.That(plan.AudioToEncode, Is.Empty, "Alle drei passen und werden nur umgepackt");
|
||||
Assert.That(plan.SubtitlesDropped, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>DTS kennt AVPlayer nicht — nur diese eine Spur wird gerechnet.</summary>
|
||||
[Test]
|
||||
public void Nur_untaugliche_Tonspuren_werden_neu_gerechnet()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload("mkv", [Bild(), Ton(1, "aac"), Ton(2, "dts"), Ton(3, "truehd")]);
|
||||
|
||||
Assert.That(plan.AudioToEncode, Is.EqualTo(new[] { 2, 3 }));
|
||||
Assert.That(plan.Reasons, Does.Contain(DeliveryReason.AudioCodecUnsupported));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Textuntertitel_werden_fuer_MP4_umgeschrieben()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload("mkv", [Bild(), Ton(1), Text(2, "subrip")]);
|
||||
|
||||
Assert.That(plan.Reasons, Does.Contain(DeliveryReason.SubtitlesNeedConversion));
|
||||
Assert.That(plan.SubtitlesDropped, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Betrifft fast jeden Blu-ray-Rip.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// PGS liegt als Bild vor, und in eine MP4 passt das nicht. Diese Spuren
|
||||
/// fallen weg — und das muss gesagt werden, statt stillschweigend zu
|
||||
/// geschehen: sonst sucht jemand nach Untertiteln, die der Server bewusst
|
||||
/// weggelassen hat.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Bilduntertitel_fallen_weg_und_das_steht_im_Plan()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload("mkv",
|
||||
[Bild(), Ton(1), Text(2, "hdmv_pgs_subtitle"), Text(3, "subrip")]);
|
||||
|
||||
Assert.That(plan.SubtitlesDropped, Is.EqualTo(new[] { 2 }));
|
||||
Assert.That(plan.Reasons, Does.Contain(DeliveryReason.ImageSubtitlesDropped));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ein_untaugliches_Bild_verlangt_ebenfalls_Umpacken()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload("mkv", [Bild("vp9"), Ton(1)]);
|
||||
Assert.That(plan.Reasons, Does.Contain(DeliveryReason.VideoCodecUnsupported));
|
||||
}
|
||||
|
||||
/// <summary>Was die Datei nicht nennt, gilt als passend.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Vorsichtshalber umzupacken hiesse, aus einer tadellosen MP4 eine neue
|
||||
/// Datei zu machen — für eine Angabe, die schlicht fehlt.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Fehlende_Angaben_loesen_kein_Umpacken_aus()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload(null,
|
||||
[Bild(null!) with { Codec = null }, Ton(1) with { Codec = null }]);
|
||||
|
||||
Assert.That(plan.Method, Is.EqualTo(DeliveryMethod.DirectFile));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Jeder_Grund_steht_nur_einmal_da()
|
||||
{
|
||||
var plan = DeliveryDecider.ForDownload("mkv", [Bild(), Ton(1, "dts"), Ton(2, "truehd"), Ton(3, "vorbis")]);
|
||||
|
||||
Assert.That(plan.Reasons.Count(g => g == DeliveryReason.AudioCodecUnsupported), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ein_eigenes_Geraeteprofil_sticht_die_Voreinstellung()
|
||||
{
|
||||
var alleskoenner = new DeviceProfile(
|
||||
new HashSet<string> { "mkv" }, new HashSet<string> { "h264" }, new HashSet<string> { "dts" });
|
||||
|
||||
var plan = DeliveryDecider.ForDownload("mkv", [Bild(), Ton(1, "dts")], alleskoenner);
|
||||
Assert.That(plan.Method, Is.EqualTo(DeliveryMethod.DirectFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using Flaro.Core.Library;
|
||||
|
||||
namespace Flaro.Core.Tests;
|
||||
|
||||
public class FilmNamenTests
|
||||
{
|
||||
[TestCase("The Matrix (1999).mkv", "The Matrix", 1999)]
|
||||
[TestCase("The.Matrix.1999.1080p.BluRay.x264-GROUP.mkv", "The Matrix", 1999)]
|
||||
[TestCase("The Matrix 1999 2160p UHD BluRay REMUX HDR.mkv", "The Matrix", 1999)]
|
||||
[TestCase("Der.Untergang.2004.German.1080p.BluRay.x264-ENCOUNTERS.mkv", "Der Untergang", 2004)]
|
||||
[TestCase("Das Boot (1981) [1080p].mp4", "Das Boot", 1981)]
|
||||
[TestCase("Inception.2010.EXTENDED.1080p.BluRay.DTS-HD.MA.5.1.x264.mkv", "Inception", 2010)]
|
||||
[TestCase("Spirited Away.m4v", "Spirited Away", null)]
|
||||
[TestCase("Männer.die.auf.Ziegen.starren.2009.German.DL.1080p.mkv", "Männer die auf Ziegen starren", 2009)]
|
||||
public void Titel_und_Jahr_werden_erkannt(string datei, string titel, int? jahr)
|
||||
{
|
||||
var name = FileNames.ParseMovie(datei);
|
||||
Assert.That(name.Title, Is.EqualTo(titel));
|
||||
Assert.That(name.Year, Is.EqualTo(jahr));
|
||||
}
|
||||
|
||||
/// <summary>Eine Zahl im Titel ist kein Jahr.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Der Fall, an dem sich die Regel entscheidet.</b> „2049" sähe wie ein
|
||||
/// Jahr aus; es liegt nur weit in der Zukunft. Ohne diese Grenze hiesse der
|
||||
/// Film „Blade Runner", erschienen 2049 — und keine Metadatenquelle der
|
||||
/// Welt findet ihn so.
|
||||
/// </remarks>
|
||||
[TestCase("Blade Runner 2049 (2017).mkv", "Blade Runner 2049", 2017)]
|
||||
[TestCase("Blade.Runner.2049.2017.1080p.mkv", "Blade Runner 2049", 2017)]
|
||||
[TestCase("2012 (2009).mkv", "2012", 2009)]
|
||||
[TestCase("1917 (2019).mkv", "1917", 2019)]
|
||||
[TestCase("300 (2006).mkv", "300", 2006)]
|
||||
public void Zahlen_im_Titel_werden_nicht_fuer_Jahre_gehalten(string datei, string titel, int jahr)
|
||||
{
|
||||
var name = FileNames.ParseMovie(datei);
|
||||
Assert.That(name.Title, Is.EqualTo(titel));
|
||||
Assert.That(name.Year, Is.EqualTo(jahr));
|
||||
}
|
||||
|
||||
/// <summary>Wer von Hand benennt, meint seine Punkte so.</summary>
|
||||
[Test]
|
||||
public void Punkte_in_einem_von_Hand_benannten_Titel_bleiben_stehen()
|
||||
{
|
||||
Assert.That(FileNames.ParseMovie("S.W.A.T. (2003).mkv").Title, Is.EqualTo("S.W.A.T."));
|
||||
Assert.That(FileNames.ParseMovie("Wall-E (2008).mkv").Title, Is.EqualTo("Wall-E"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ohne_erkennbares_Muster_bleibt_der_Dateiname_stehen()
|
||||
{
|
||||
// Lieber ein unschöner Titel als ein leerer: so ist der Film wenigstens
|
||||
// zu finden und von Hand zu berichtigen.
|
||||
Assert.That(FileNames.ParseMovie("irgendwas.mkv").Title, Is.EqualTo("irgendwas"));
|
||||
Assert.That(FileNames.ParseMovie("1080p.mkv").Title, Is.EqualTo("1080p"));
|
||||
}
|
||||
}
|
||||
|
||||
public class FolgenNamenTests
|
||||
{
|
||||
[TestCase("Notruf.Hafenkante.S01E01.German.1080p.WEB-DL.x264-GRP.mkv", "Notruf Hafenkante", 1, 1)]
|
||||
[TestCase("Notruf Hafenkante - S01E01 - Der Fall.mkv", "Notruf Hafenkante", 1, 1)]
|
||||
[TestCase("Breaking Bad S05E14 Ozymandias.mkv", "Breaking Bad", 5, 14)]
|
||||
[TestCase("The.Wire.s02e05.1080p.mkv", "The Wire", 2, 5)]
|
||||
[TestCase("Dark - 1x03 - Gestern und heute.mkv", "Dark", 1, 3)]
|
||||
[TestCase("Tatort.01x09.mkv", "Tatort", 1, 9)]
|
||||
[TestCase("Show S1E2.mkv", "Show", 1, 2)]
|
||||
[TestCase("Serie.S01.E07.1080p.mkv", "Serie", 1, 7)]
|
||||
public void Serie_Staffel_und_Folge_werden_erkannt(string datei, string serie, int staffel, int folge)
|
||||
{
|
||||
var name = FileNames.ParseEpisode(datei);
|
||||
Assert.That(name, Is.Not.Null, $"„{datei}“ wurde gar nicht als Folge erkannt");
|
||||
Assert.That(name!.SeriesTitle, Is.EqualTo(serie));
|
||||
Assert.That(name.Season, Is.EqualTo(staffel));
|
||||
Assert.That(name.Episode, Is.EqualTo(folge));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Der_Folgentitel_wird_mitgenommen_wenn_einer_dasteht()
|
||||
{
|
||||
var name = FileNames.ParseEpisode("Notruf Hafenkante - S01E01 - Der Fall.mkv");
|
||||
Assert.That(name!.EpisodeTitle, Is.EqualTo("Der Fall"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Technik_hinter_der_Nummer_ist_kein_Folgentitel()
|
||||
{
|
||||
var name = FileNames.ParseEpisode("Breaking.Bad.S05E14.1080p.BluRay.x264-GRP.mkv");
|
||||
Assert.That(name!.EpisodeTitle, Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>Doppelfolgen zählen ab der ersten.</summary>
|
||||
[Test]
|
||||
public void Eine_Doppelfolge_wird_der_ersten_Nummer_zugeordnet()
|
||||
{
|
||||
var name = FileNames.ParseEpisode("Serie.S02E05E06.mkv");
|
||||
Assert.That(name!.Season, Is.EqualTo(2));
|
||||
Assert.That(name.Episode, Is.EqualTo(5));
|
||||
}
|
||||
|
||||
/// <summary>Ohne Nummer wird nichts geraten.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Eine erfundene Staffel 1 wäre schlimmer als gar keine Angabe: der Titel
|
||||
/// landete unter einer Serie, zu der er nicht gehört, und niemand fände ihn
|
||||
/// dort je wieder.
|
||||
/// </remarks>
|
||||
[TestCase("The Matrix (1999).mkv")]
|
||||
[TestCase("Irgendein Film.mkv")]
|
||||
[TestCase("Dokumentation ueber 1999.mkv")]
|
||||
public void Ohne_Folgennummer_gibt_es_keine_Folge(string datei)
|
||||
{
|
||||
Assert.That(FileNames.ParseEpisode(datei), Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>Eine Auflösung ist keine Folgennummer.</summary>
|
||||
[Test]
|
||||
public void Die_Aufloesung_wird_nicht_fuer_eine_Folge_gehalten()
|
||||
{
|
||||
// „1920x1080" sähe in der Kreuzschreibweise wie Staffel 1920 aus.
|
||||
Assert.That(FileNames.ParseEpisode("Film.1920x1080.mkv"), Is.Null);
|
||||
}
|
||||
}
|
||||
|
||||
public class MedienDateienTests
|
||||
{
|
||||
[TestCase("film.mkv", true)]
|
||||
[TestCase("film.MP4", true)]
|
||||
[TestCase("film.m2ts", true)]
|
||||
[TestCase("film.nfo", false)]
|
||||
[TestCase("film.srt", false)]
|
||||
[TestCase("poster.jpg", false)]
|
||||
[TestCase("Thumbs.db", false)]
|
||||
[TestCase(".DS_Store", false)]
|
||||
[TestCase(".versteckt.mkv", false)]
|
||||
public void Nur_Videodateien_zaehlen(string name, bool erwartet)
|
||||
{
|
||||
Assert.That(MediaExtensions.IsVideo(name), Is.EqualTo(erwartet));
|
||||
}
|
||||
|
||||
/// <summary>Ein laufender Download ist noch kein Film.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Ihn aufzunehmen hiesse: ein Titel erscheint, verschwindet wieder, und
|
||||
/// wer ihn dazwischen antippt, sieht einen Film, der mittendrin abbricht.
|
||||
/// </remarks>
|
||||
[TestCase("film.mkv.part")]
|
||||
[TestCase("film.mkv.!qb")]
|
||||
[TestCase("film.mkv.crdownload")]
|
||||
public void Unfertige_Dateien_zaehlen_nicht(string name)
|
||||
{
|
||||
Assert.That(MediaExtensions.IsVideo(name), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>@eaDir enthält Kopien jeder Datei — mitscannen verdoppelt die Mediathek.</summary>
|
||||
[TestCase("@eaDir", true)]
|
||||
[TestCase("#recycle", true)]
|
||||
[TestCase(".git", true)]
|
||||
[TestCase("Extras", true)]
|
||||
[TestCase("Season 01", false)]
|
||||
[TestCase("Filme", false)]
|
||||
public void Bestimmte_Ordner_werden_uebersprungen(string name, bool erwartet)
|
||||
{
|
||||
Assert.That(MediaExtensions.IsSkippedDirectory(name), Is.EqualTo(erwartet));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.7.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="NUnit.Framework" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Flaro.Core\Flaro.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,152 @@
|
||||
using Flaro.Core.Library;
|
||||
|
||||
namespace Flaro.Core.Tests;
|
||||
|
||||
public class LibraryWalkerTests
|
||||
{
|
||||
private string _wurzel = "";
|
||||
|
||||
[SetUp]
|
||||
public void Anlegen() => _wurzel = Directory.CreateDirectory(
|
||||
Path.Combine(Path.GetTempPath(), "flaro-bestand-" + Guid.CreateVersion7().ToString("N"))).FullName;
|
||||
|
||||
[TearDown]
|
||||
public void Aufraeumen()
|
||||
{
|
||||
// Ein Test sperrt absichtlich einen Ordner; ohne Zurücksetzen liesse
|
||||
// er sich nicht mehr löschen.
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
foreach (var d in Directory.EnumerateDirectories(_wurzel, "*", SearchOption.AllDirectories))
|
||||
File.SetUnixFileMode(d, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
|
||||
}
|
||||
try { Directory.Delete(_wurzel, recursive: true); } catch (IOException) { }
|
||||
}
|
||||
|
||||
private void Datei(string relativ, int bytes = 1024)
|
||||
{
|
||||
var voll = Path.Combine(_wurzel, relativ);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
|
||||
File.WriteAllBytes(voll, new byte[bytes]);
|
||||
}
|
||||
|
||||
private string[] GefundeneNamen(WalkResult ergebnis) =>
|
||||
ergebnis.Files.Select(f => Path.GetFileName(f.Path)).Order().ToArray();
|
||||
|
||||
[Test]
|
||||
public void Videodateien_werden_gefunden_Beiwerk_nicht()
|
||||
{
|
||||
Datei("Filme/The Matrix (1999).mkv");
|
||||
Datei("Filme/The Matrix (1999).srt");
|
||||
Datei("Filme/poster.jpg");
|
||||
Datei("Filme/The Matrix (1999).nfo");
|
||||
|
||||
Assert.That(GefundeneNamen(LibraryWalker.Walk(_wurzel)), Is.EqualTo(new[] { "The Matrix (1999).mkv" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Auch_tief_verschachtelt_wird_gefunden()
|
||||
{
|
||||
Datei("Serien/Dark/Staffel 1/Dark - S01E01.mkv");
|
||||
Datei("Serien/Dark/Staffel 2/Dark - S02E01.mkv");
|
||||
|
||||
Assert.That(LibraryWalker.Walk(_wurzel).Files, Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
/// <summary>Der Fund, der eine Mediathek sonst verdoppelt.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>@eaDir ist Synologys Vorschauablage und enthält eine Kopie jeder
|
||||
/// Datei.</b> Mit <c>SearchOption.AllDirectories</c> — der bequemen
|
||||
/// Fassung — liefe der Scan hinein und fände jeden Film zweimal, einmal
|
||||
/// davon als unbrauchbare Kopie. Genau deshalb steigt der Walker von Hand
|
||||
/// ab.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Vorschauablagen_und_Papierkoerbe_werden_uebersprungen()
|
||||
{
|
||||
Datei("Filme/The Matrix (1999).mkv");
|
||||
Datei("Filme/@eaDir/The Matrix (1999).mkv/SYNOPHOTO_THUMB.mkv");
|
||||
Datei("Filme/#recycle/Alter Film (2001).mkv");
|
||||
Datei("Filme/.versteckt/Noch einer (2002).mkv");
|
||||
Datei("Filme/Extras/Making Of.mkv");
|
||||
|
||||
Assert.That(GefundeneNamen(LibraryWalker.Walk(_wurzel)), Is.EqualTo(new[] { "The Matrix (1999).mkv" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Groesse_und_Aenderungszeit_kommen_mit()
|
||||
{
|
||||
Datei("Filme/Film (2000).mkv", bytes: 4096);
|
||||
var gefunden = LibraryWalker.Walk(_wurzel).Files.Single();
|
||||
|
||||
Assert.That(gefunden.SizeBytes, Is.EqualTo(4096));
|
||||
Assert.That(gefunden.ModifiedAt, Is.GreaterThan(DateTimeOffset.UtcNow.AddMinutes(-5)));
|
||||
}
|
||||
|
||||
/// <summary>Ein gesperrter Ordner darf nicht den ganzen Lauf verderben.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Sonst entschiede ein einzelner falsch gesetzter Unterordner darüber, ob
|
||||
/// überhaupt etwas gefunden wird — und die Mediathek bliebe leer, ohne dass
|
||||
/// jemand sagen könnte, warum.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Ein_gesperrter_Ordner_wird_vermerkt_und_uebersprungen()
|
||||
{
|
||||
if (OperatingSystem.IsWindows()) Assert.Ignore("Dateirechte verhalten sich dort anders.");
|
||||
|
||||
Datei("Filme/Erreichbar (2000).mkv");
|
||||
var gesperrt = Directory.CreateDirectory(Path.Combine(_wurzel, "Filme", "gesperrt")).FullName;
|
||||
File.WriteAllBytes(Path.Combine(gesperrt, "Versteckt (2001).mkv"), new byte[10]);
|
||||
if (OperatingSystem.IsWindows()) return;
|
||||
File.SetUnixFileMode(gesperrt, UnixFileMode.None);
|
||||
|
||||
var ergebnis = LibraryWalker.Walk(_wurzel);
|
||||
|
||||
Assert.That(GefundeneNamen(ergebnis), Is.EqualTo(new[] { "Erreichbar (2000).mkv" }));
|
||||
Assert.That(ergebnis.Problems, Has.Count.EqualTo(1));
|
||||
Assert.That(ergebnis.Problems[0].Path, Is.EqualTo(gesperrt));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ein_Verzeichnis_das_es_nicht_gibt_wird_gemeldet_statt_zu_werfen()
|
||||
{
|
||||
var ergebnis = LibraryWalker.Walk(Path.Combine(_wurzel, "gibtesnicht"));
|
||||
|
||||
Assert.That(ergebnis.Files, Is.Empty);
|
||||
Assert.That(ergebnis.Problems, Has.Count.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ein_leerer_Bestand_ist_kein_Fehler()
|
||||
{
|
||||
var ergebnis = LibraryWalker.Walk(_wurzel);
|
||||
Assert.That(ergebnis.Files, Is.Empty);
|
||||
Assert.That(ergebnis.Problems, Is.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public class SortTitleTests
|
||||
{
|
||||
[TestCase("Der Untergang", "Untergang")]
|
||||
[TestCase("Die Welle", "Welle")]
|
||||
[TestCase("Das Boot", "Boot")]
|
||||
[TestCase("The Matrix", "Matrix")]
|
||||
[TestCase("A Quiet Place", "Quiet Place")]
|
||||
[TestCase("An American Werewolf", "American Werewolf")]
|
||||
[TestCase("Blade Runner", "Blade Runner")]
|
||||
[TestCase("Theater", "Theater")]
|
||||
public void Der_Artikel_faellt_fuers_Sortieren_weg(string titel, string erwartet)
|
||||
{
|
||||
Assert.That(SortTitle.For(titel), Is.EqualTo(erwartet));
|
||||
}
|
||||
|
||||
/// <summary>Ein Titel, der nur aus einem Artikel besteht, bleibt stehen.</summary>
|
||||
[TestCase("Die")]
|
||||
[TestCase("The")]
|
||||
public void Ein_blosser_Artikel_bleibt_als_Titel_erhalten(string titel)
|
||||
{
|
||||
Assert.That(SortTitle.For(titel), Is.EqualTo(titel));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Flaro.Core.Users;
|
||||
|
||||
namespace Flaro.Core.Tests;
|
||||
|
||||
public class PasswordHasherTests
|
||||
{
|
||||
[Test]
|
||||
public void Richtiges_Passwort_wird_angenommen()
|
||||
{
|
||||
var hash = PasswordHasher.Hash("geheim-und-lang");
|
||||
Assert.That(PasswordHasher.Verify("geheim-und-lang", hash), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Falsches_Passwort_wird_abgelehnt()
|
||||
{
|
||||
var hash = PasswordHasher.Hash("geheim-und-lang");
|
||||
Assert.That(PasswordHasher.Verify("geheim-und-kurz", hash), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>Gleiches Passwort, zwei Hashes — sonst wäre das Salz wirkungslos.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Ohne diese Eigenschaft verrät die Datenbank, welche Konten dasselbe
|
||||
/// Passwort benutzen, und eine einmal gebaute Tabelle knackt alle auf
|
||||
/// einen Schlag.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Zweimal_dasselbe_Passwort_ergibt_zwei_verschiedene_Hashes()
|
||||
{
|
||||
var a = PasswordHasher.Hash("dasselbe");
|
||||
var b = PasswordHasher.Hash("dasselbe");
|
||||
Assert.That(a, Is.Not.EqualTo(b));
|
||||
Assert.That(PasswordHasher.Verify("dasselbe", a), Is.True);
|
||||
Assert.That(PasswordHasher.Verify("dasselbe", b), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Der_Hash_nennt_seine_Kennwerte()
|
||||
{
|
||||
var hash = PasswordHasher.Hash("egal");
|
||||
Assert.That(hash, Does.StartWith("$argon2id$v=19$m=19456,t=2,p=1$"));
|
||||
}
|
||||
|
||||
/// <summary>Eine beschädigte Zeile lehnt ab, statt den Server umzuwerfen.</summary>
|
||||
[TestCase("")]
|
||||
[TestCase("kein-hash")]
|
||||
[TestCase("$argon2id$v=19$m=19456,t=2,p=1$nur-vier-teile")]
|
||||
[TestCase("$bcrypt$v=19$m=19456,t=2,p=1$c2FsenNhbHo$aGFzaA")]
|
||||
[TestCase("$argon2id$v=19$m=x,t=2,p=1$c2FsenNhbHo$aGFzaA")]
|
||||
[TestCase("$argon2id$v=19$m=19456,t=2,p=1$!!!kein-base64!!!$aGFzaA")]
|
||||
public void Unlesbarer_Hash_wirft_nicht_sondern_lehnt_ab(string kaputt)
|
||||
{
|
||||
Assert.That(PasswordHasher.Verify("egal", kaputt), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Leeres_Passwort_wird_abgelehnt_statt_gehasht()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => PasswordHasher.Hash(""));
|
||||
Assert.That(PasswordHasher.Verify("", PasswordHasher.Hash("echt")), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>Alte, schwächere Hashes sollen beim nächsten Anmelden auffallen.</summary>
|
||||
[Test]
|
||||
public void Schwaechere_Kennwerte_verlangen_ein_Neuhashen()
|
||||
{
|
||||
Assert.That(PasswordHasher.NeedsRehash("$argon2id$v=19$m=4096,t=1,p=1$c2FsenNhbHo$aGFzaA"), Is.True);
|
||||
Assert.That(PasswordHasher.NeedsRehash("$2a$10$irgendwas"), Is.True);
|
||||
Assert.That(PasswordHasher.NeedsRehash(PasswordHasher.Hash("aktuell")), Is.False);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using Flaro.Core.Library;
|
||||
|
||||
namespace Flaro.Core.Tests;
|
||||
|
||||
/// <summary>Was Flaro in einen Medienordner schreiben darf.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Die wichtigste Prüfdatei des Projekts.</b> Damit Plakate neben dem Film
|
||||
/// landen können, ist die Einhängung schreibbar — und damit hängt der Schutz
|
||||
/// des gesamten Bestands an dieser einen Liste.
|
||||
/// </remarks>
|
||||
public class SidecarNamesTests
|
||||
{
|
||||
[TestCase("poster.jpg")]
|
||||
[TestCase("fanart.jpg")]
|
||||
[TestCase("logo.png")]
|
||||
[TestCase("movie.nfo")]
|
||||
[TestCase("tvshow.nfo")]
|
||||
[TestCase("Dark - S01E01.nfo")]
|
||||
[TestCase("Dark - S01E01-thumb.jpg")]
|
||||
[TestCase("POSTER.JPG")]
|
||||
public void Beiwerk_darf_geschrieben_werden(string name)
|
||||
{
|
||||
Assert.That(SidecarNames.MayWrite(name), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>Eine Mediendatei wird nie angefasst — auch nicht, wenn jemand es verlangt.</summary>
|
||||
[TestCase("film.mkv")]
|
||||
[TestCase("Der Untergang (2004).mp4")]
|
||||
[TestCase("film.mkv.nfo.mkv")]
|
||||
public void Mediendateien_niemals(string name)
|
||||
{
|
||||
Assert.That(SidecarNames.MayWrite(name), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>Kein Weg aus dem Ordner heraus.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Käme hier ein Pfadbestandteil durch, liesse sich über einen
|
||||
/// erfundenen „Dateinamen" in jedes Verzeichnis des Containers
|
||||
/// schreiben — auch in das, in dem die Datenbank liegt.
|
||||
/// </remarks>
|
||||
[TestCase("../poster.jpg")]
|
||||
[TestCase("../../etc/passwd")]
|
||||
[TestCase("/etc/poster.jpg")]
|
||||
[TestCase("unterordner/poster.jpg")]
|
||||
[TestCase("..\\poster.jpg")]
|
||||
[TestCase("ordner\\movie.nfo")]
|
||||
[TestCase("..movie.nfo")]
|
||||
public void Kein_Pfad_kommt_durch(string versuch)
|
||||
{
|
||||
Assert.That(SidecarNames.MayWrite(versuch), Is.False);
|
||||
}
|
||||
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
[TestCase(".nfo")]
|
||||
[TestCase("-thumb.jpg")]
|
||||
[TestCase(".versteckt.nfo")]
|
||||
[TestCase("poster.png")]
|
||||
[TestCase("beliebig.txt")]
|
||||
public void Alles_andere_wird_abgelehnt(string versuch)
|
||||
{
|
||||
Assert.That(SidecarNames.MayWrite(versuch), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>Zwei Filme im selben Ordner dürfen sich nicht überschreiben.</summary>
|
||||
[Test]
|
||||
public void Beiwerk_mit_Dateinamen_davor_ist_auch_erlaubt()
|
||||
{
|
||||
Assert.That(SidecarNames.Prefixed("Matrix (1999).mkv", SidecarNames.MoviePoster),
|
||||
Is.EqualTo("Matrix (1999)-poster.jpg"));
|
||||
|
||||
foreach (var beiwerk in new[] { SidecarNames.MoviePoster, SidecarNames.MovieBackdrop, SidecarNames.MovieLogo })
|
||||
Assert.That(SidecarNames.MayWrite(SidecarNames.Prefixed("Matrix (1999).mkv", beiwerk)), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Folgen_Beiwerk_heisst_wie_die_Folge()
|
||||
{
|
||||
Assert.That(SidecarNames.EpisodeNfo("Dark - S01E01.mkv"), Is.EqualTo("Dark - S01E01.nfo"));
|
||||
Assert.That(SidecarNames.EpisodeThumb("Dark - S01E01.mkv"), Is.EqualTo("Dark - S01E01-thumb.jpg"));
|
||||
}
|
||||
|
||||
/// <summary>Was erzeugt wird, muss auch erlaubt sein.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Die beiden Hälften könnten auseinanderlaufen — jemand ändert das Muster
|
||||
/// für Folgen und vergisst die Liste. Dann schriebe Flaro nichts mehr und
|
||||
/// niemand wüsste, warum.
|
||||
/// </remarks>
|
||||
[TestCase("Dark - S01E01.mkv")]
|
||||
[TestCase("Notruf.Hafenkante.S01E01.German.1080p.mkv")]
|
||||
[TestCase("Serie 1x01.avi")]
|
||||
public void Die_erzeugten_Namen_stehen_auf_der_Liste(string mediendatei)
|
||||
{
|
||||
Assert.That(SidecarNames.MayWrite(SidecarNames.EpisodeNfo(mediendatei)), Is.True);
|
||||
Assert.That(SidecarNames.MayWrite(SidecarNames.EpisodeThumb(mediendatei)), Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
public class ProviderIdParserTests
|
||||
{
|
||||
[Test]
|
||||
public void Eine_TMDB_Kennung_im_Ordnernamen_wird_gelesen()
|
||||
{
|
||||
var kennungen = ProviderIdParser.FromPath("/media/Filme/Der Untergang (2004) [tmdbid-613]/film.mkv");
|
||||
Assert.That(kennungen.Tmdb, Is.EqualTo(613));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Eine_IMDb_Kennung_wird_gelesen_und_kleingeschrieben()
|
||||
{
|
||||
var kennungen = ProviderIdParser.FromPath("/media/Filme/Matrix (1999) [imdbid-TT0133093]/film.mkv");
|
||||
Assert.That(kennungen.Imdb, Is.EqualTo("tt0133093"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Beide_nebeneinander_gehen_auch()
|
||||
{
|
||||
var kennungen = ProviderIdParser.FromPath("/m/Film (2004) [tmdbid-613] [imdbid-tt0363163]/f.mkv");
|
||||
Assert.That((kennungen.Tmdb, kennungen.Imdb), Is.EqualTo((613, "tt0363163")));
|
||||
}
|
||||
|
||||
[TestCase("/media/Filme/Der Untergang (2004)/film.mkv")]
|
||||
[TestCase("/media/Filme/Film [1080p]/film.mkv")]
|
||||
[TestCase("")]
|
||||
[TestCase("/media/Film [tmdbid-]/f.mkv")]
|
||||
[TestCase("/media/Film [imdbid-nichts]/f.mkv")]
|
||||
public void Ohne_Kennung_kommt_nichts_zurueck(string pfad)
|
||||
{
|
||||
Assert.That(ProviderIdParser.FromPath(pfad).Any, Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
public class NfoTests
|
||||
{
|
||||
[Test]
|
||||
public void Ein_Film_wird_geschrieben_und_wieder_gelesen()
|
||||
{
|
||||
var xml = Nfo.Write(new MovieNfo(
|
||||
"Der Untergang", 2004, "Berlin, April 1945.",
|
||||
new ProviderIds(613, "tt0363163"), Locked: true));
|
||||
|
||||
Assert.That(xml, Does.StartWith("<?xml"));
|
||||
Assert.That(xml, Does.Contain("<title>Der Untergang</title>"));
|
||||
Assert.That(xml, Does.Contain("<year>2004</year>"));
|
||||
|
||||
var gelesen = Nfo.ReadIds(xml);
|
||||
Assert.That((gelesen.Tmdb, gelesen.Imdb), Is.EqualTo((613, "tt0363163")));
|
||||
Assert.That(Nfo.ReadLocked(xml), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>Kennungen stehen doppelt drin, für alte wie neue Werkzeuge.</summary>
|
||||
[Test]
|
||||
public void Die_Kennungen_stehen_in_beiden_Formen_da()
|
||||
{
|
||||
var xml = Nfo.Write(new MovieNfo("Egal", null, null, new ProviderIds(613, "tt0363163"), false));
|
||||
|
||||
Assert.That(xml, Does.Contain("""<uniqueid type="tmdb" default="true">613</uniqueid>"""));
|
||||
Assert.That(xml, Does.Contain("<tmdbid>613</tmdbid>"));
|
||||
Assert.That(xml, Does.Contain("""<uniqueid type="imdb">tt0363163</uniqueid>"""));
|
||||
Assert.That(xml, Does.Contain("<imdbid>tt0363163</imdbid>"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Die_aeltere_Form_allein_wird_auch_gelesen()
|
||||
{
|
||||
var gelesen = Nfo.ReadIds("<movie><tmdbid>613</tmdbid><imdbid>tt0363163</imdbid></movie>");
|
||||
Assert.That((gelesen.Tmdb, gelesen.Imdb), Is.EqualTo((613, "tt0363163")));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sonderzeichen_im_Titel_werden_richtig_geschrieben()
|
||||
{
|
||||
var xml = Nfo.Write(new MovieNfo("Tom & Jerry <der Film>", null, null, ProviderIds.None, false));
|
||||
|
||||
Assert.That(xml, Does.Contain("Tom & Jerry <der Film>"));
|
||||
Assert.That(Nfo.ReadIds(xml).Any, Is.False, "Und die Datei bleibt lesbar");
|
||||
}
|
||||
|
||||
/// <summary>Eine von Hand geschriebene NFO ist oft kaputt — das darf nichts abbrechen.</summary>
|
||||
[TestCase("")]
|
||||
[TestCase("kein xml")]
|
||||
[TestCase("<movie><title>offen")]
|
||||
[TestCase("<movie><tmdbid>keine zahl</tmdbid></movie>")]
|
||||
public void Unlesbares_ergibt_nichts_statt_eines_Absturzes(string kaputt)
|
||||
{
|
||||
Assert.That(Nfo.ReadIds(kaputt).Any, Is.False);
|
||||
Assert.That(Nfo.ReadLocked(kaputt), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>Eine NFO aus dem Medienordner hat Flaro nicht geschrieben.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Ohne abgeschaltete DTDs wäre das ein Weg, fremde Dateien zu lesen.</b>
|
||||
/// Eine untergeschobene NFO mit einer externen Entität könnte den Server
|
||||
/// dazu bringen, <c>/config/flaro.db</c> einzulesen und den Inhalt als
|
||||
/// Titel auszuliefern.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Eine_NFO_mit_externer_Entitaet_wird_nicht_ausgewertet()
|
||||
{
|
||||
var angriff = """
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE movie [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
<movie><title>&xxe;</title><tmdbid>613</tmdbid></movie>
|
||||
""";
|
||||
|
||||
Assert.That(Nfo.ReadIds(angriff).Any, Is.False, "Die Datei wird gar nicht erst verarbeitet");
|
||||
Assert.That(Nfo.ReadLocked(angriff), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Eine_Folge_bekommt_Staffel_und_Nummer()
|
||||
{
|
||||
var xml = Nfo.Write(new EpisodeNfo("Geheimnisse", 1, 1, "Ein Junge verschwindet."));
|
||||
|
||||
Assert.That(xml, Does.Contain("<season>1</season>"));
|
||||
Assert.That(xml, Does.Contain("<episode>1</episode>"));
|
||||
Assert.That(xml, Does.Contain("<title>Geheimnisse</title>"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user