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>
138 lines
5.9 KiB
C#
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"));
|
|
}
|
|
}
|