Files
Flaro/server/tests/Flaro.Api.Tests/CodeSignInTests.cs
T
ScarriffleandClaude Opus 5 a7cf6c5def 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>
2026-10-03 19:38:27 +02:00

138 lines
5.9 KiB
C#

using System.Net;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
namespace Flaro.Api.Tests;
/// <summary>Anmelden am Fernseher, ohne ein Passwort zu tippen.</summary>
public class CodeSignInTests
{
private static Task<HttpResponseMessage> CodeAnfordern(FlaroServer server) =>
server.Client.PostAsJsonAsync("/api/v1/auth/code", new CodeRequest("Wohnzimmer", "tv-1"));
[Test]
public async Task Der_ganze_Weg_vom_Bildschirm_zum_Token()
{
using var server = new FlaroServer();
var telefon = await server.SetUpAndSignIn();
// 1. Der Fernseher fordert an und zeigt den Code.
var angefordert = await (await CodeAnfordern(server)).Content.ReadFromJsonAsync<CodeResponse>();
Assert.That(angefordert!.Code, Does.Match("^[0-9]{6}$"));
// 2. Noch hat niemand bestätigt.
var wartend = await server.Client.GetFromJsonAsync<CodeState>($"/api/v1/auth/code/{angefordert.Handle}");
Assert.That(wartend!.State, Is.EqualTo("pending"));
Assert.That(wartend.Login, Is.Null);
// 3. Am Telefon bestätigen.
var bestätigt = await server.AlsAngemeldeter(telefon.Token)
.PostAsync($"/api/v1/auth/code/{angefordert.Code}", null);
bestätigt.EnsureSuccessStatusCode();
// 4. Der Fernseher holt sein Token ab.
var fertig = await server.Client.GetFromJsonAsync<CodeState>($"/api/v1/auth/code/{angefordert.Handle}");
Assert.That(fertig!.State, Is.EqualTo("approved"));
Assert.That(fertig.Login!.Token, Is.Not.Empty);
// Und es gilt auch wirklich.
var ich = await server.AlsAngemeldeter(fertig.Login.Token)
.GetFromJsonAsync<AuthenticatedUser>("/api/v1/auth/me");
Assert.That(ich!.UserName, Is.EqualTo("guido"));
}
/// <summary>Der Code allein darf niemals ein Token herausgeben.</summary>
///
/// <remarks>
/// <b>Die wichtigste Prüfung dieser Datei.</b> Gäbe der Server auf einen
/// erratenen Abholschein hin Token heraus, wären sechsstellige Codes ein
/// Türschloss mit einer Million Schlüsseln — in Minuten durchprobiert, und
/// am Ende stünde ein fremdes Gerät in der Mediathek.
/// </remarks>
[Test]
public async Task Ein_fremder_Abholschein_bekommt_nichts()
{
using var server = new FlaroServer();
var telefon = await server.SetUpAndSignIn();
var echt = await (await CodeAnfordern(server)).Content.ReadFromJsonAsync<CodeResponse>();
await server.AlsAngemeldeter(telefon.Token).PostAsync($"/api/v1/auth/code/{echt!.Code}", null);
// Der Code ist bestätigt — aber der Abholschein stimmt nicht.
var erraten = await server.Client.GetFromJsonAsync<CodeState>("/api/v1/auth/code/ausgedacht");
Assert.That(erraten!.State, Is.EqualTo("expired"));
Assert.That(erraten.Login, Is.Null);
}
/// <summary>Auch der Code selbst taugt nicht als Abholschein.</summary>
[Test]
public async Task Der_Code_ist_kein_Abholschein()
{
using var server = new FlaroServer();
var telefon = await server.SetUpAndSignIn();
var echt = await (await CodeAnfordern(server)).Content.ReadFromJsonAsync<CodeResponse>();
await server.AlsAngemeldeter(telefon.Token).PostAsync($"/api/v1/auth/code/{echt!.Code}", null);
var mitCode = await server.Client.GetFromJsonAsync<CodeState>($"/api/v1/auth/code/{echt.Code}");
Assert.That(mitCode!.Login, Is.Null);
}
[Test]
public async Task Ein_abgeholtes_Token_gibt_es_kein_zweites_Mal()
{
using var server = new FlaroServer();
var telefon = await server.SetUpAndSignIn();
var angefordert = await (await CodeAnfordern(server)).Content.ReadFromJsonAsync<CodeResponse>();
await server.AlsAngemeldeter(telefon.Token).PostAsync($"/api/v1/auth/code/{angefordert!.Code}", null);
var erstesMal = await server.Client.GetFromJsonAsync<CodeState>($"/api/v1/auth/code/{angefordert.Handle}");
var zweitesMal = await server.Client.GetFromJsonAsync<CodeState>($"/api/v1/auth/code/{angefordert.Handle}");
Assert.That(erstesMal!.State, Is.EqualTo("approved"));
Assert.That(zweitesMal!.State, Is.EqualTo("expired"));
Assert.That(zweitesMal.Login, Is.Null);
}
/// <summary>Bestätigen darf nur, wer angemeldet ist.</summary>
[Test]
public async Task Ein_Unangemeldeter_kann_nichts_bestaetigen()
{
using var server = new FlaroServer();
await server.SetUpAndSignIn();
var angefordert = await (await CodeAnfordern(server)).Content.ReadFromJsonAsync<CodeResponse>();
var versuch = await server.Client.PostAsync($"/api/v1/auth/code/{angefordert!.Code}", null);
Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
var danach = await server.Client.GetFromJsonAsync<CodeState>($"/api/v1/auth/code/{angefordert.Handle}");
Assert.That(danach!.State, Is.EqualTo("pending"));
}
[Test]
public async Task Ein_erfundener_Code_laesst_sich_nicht_bestaetigen()
{
using var server = new FlaroServer();
var telefon = await server.SetUpAndSignIn();
var versuch = await server.AlsAngemeldeter(telefon.Token).PostAsync("/api/v1/auth/code/000000", null);
Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
/// <summary>Am Telefon soll stehen, was man da hereinlässt.</summary>
[Test]
public async Task Die_Bestaetigung_nennt_das_Geraet()
{
using var server = new FlaroServer();
var telefon = await server.SetUpAndSignIn();
var angefordert = await (await CodeAnfordern(server)).Content.ReadFromJsonAsync<CodeResponse>();
var antwort = await server.AlsAngemeldeter(telefon.Token)
.PostAsync($"/api/v1/auth/code/{angefordert!.Code}", null);
Assert.That(await antwort.Content.ReadAsStringAsync(), Does.Contain("Wohnzimmer"));
}
}