feat: Fuenfte Stufe der iOS-App - Kategorien, Schlagwoerter, Bank, zweiter Faktor
Die Nebenraeume haengen unter Einstellungen, nicht als eigene Reiter: Man braucht sie selten, und eine Leiste mit zehn Reitern findet niemand mehr. - Kategorien: anlegen, aendern, archivieren, loeschen - samt Gruppen. Eine Kategorie oeffnet ihre Erkennungswoerter, die sich dort hinzufuegen und entfernen lassen. Darunter steht, was sonst niemand merkt: Sie wirken nur auf kuenftige Importe. - Woerter, die oft korrigiert werden, stehen als Pruefmeldung in den Einstellungen - behalten oder entfernen. - Schlagwoerter: anlegen, umbenennen, umfaerben, loeschen. Neue bekommen reihum eine Farbe, damit nicht alle grau sind. - Einstellungen: Erscheinungsbild, Hauptwaehrung und Budget-Starttag lassen sich jetzt aendern. Ohne Sichern-Knopf - es ist je Zeile genau ein Wert, und das Backend nimmt ihn einzeln entgegen. Dazu der Hinweis, dass sie dem Konto gehoeren und nicht dem Geraet. - Bank: abrufen und Salden auffrischen. Verbinden bleibt am Rechner, die Bank leitet fest dorthin zurueck. Der letzte Fehler der Verbindung steht dabei, sonst ruft man vergeblich ab. - Zweiter Faktor: einrichten in drei Schritten. Auf dem Telefon fuehrt der Weg ueber die otpauth-Adresse direkt in die Authenticator-App statt ueber einen QR-Code, den man sich selbst zeigen muesste. Die Rettungscodes erscheinen einmal; das Blatt laesst sich nicht wegwischen und erst schliessen, wenn man bestaetigt hat, sie gesichert zu haben. Abschalten des zweiten Faktors bleibt bewusst draussen: Es verlangt das Kontopasswort, und die App speichert keines. Ein Vollzugang auf diesem Geraet soll den Schutz des Kontos nicht aufheben koennen. 59 Tests gruen. Texte in Deutsch und Englisch, zeilengleich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -744,3 +744,77 @@ extension ServerantwortenTests {
|
||||
#expect(ids.count == vorschlaege.vorschlaege.count)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Nebenraeume
|
||||
|
||||
@Suite("Nebenraeume")
|
||||
struct NebenraeumeTests {
|
||||
|
||||
private func felder<A: Encodable>(_ wert: A) throws -> [String: Any] {
|
||||
let roh = try JSONEncoder().encode(wert)
|
||||
return try #require(JSONSerialization.jsonObject(with: roh) as? [String: Any])
|
||||
}
|
||||
|
||||
@Test("Eine Teilaenderung an Kategorie und Tag bleibt teilweise")
|
||||
func teilaenderungen() throws {
|
||||
// Dieselbe Falle wie bei Buchung und Konto: Ein nicht angefasstes
|
||||
// Feld als null zu schicken loeschte Symbol, Farbe und Gruppe mit.
|
||||
#expect(try felder(KategorieAendern(isArchived: true)).keys.sorted()
|
||||
== ["is_archived"])
|
||||
#expect(try felder(TagAendern(color: "#2a78d6")).keys.sorted() == ["color"])
|
||||
}
|
||||
|
||||
@Test("Die Feldnamen der Kategorie sind die des Backends")
|
||||
func kategorieFeldnamen() throws {
|
||||
let neu = try felder(KategorieAnlegen(groupId: 2, name: "Spartopf",
|
||||
spendingIsGood: true))
|
||||
#expect(neu["group_id"] as? Int == 2)
|
||||
#expect(neu["spending_is_good"] as? Bool == true)
|
||||
#expect(neu["excluded_from_stats"] as? Bool == false)
|
||||
}
|
||||
|
||||
@Test("Ein Abruf ohne zugeordnetes Konto sagt das auch")
|
||||
func abrufOhneZuordnung() throws {
|
||||
// Sonst meldet die App "0 Umsaetze geholt" und der Nutzer sucht den
|
||||
// Fehler bei der Bank statt bei der fehlenden Zuordnung.
|
||||
let roh = """
|
||||
{"geholt": 0, "neu": 0, "saldi": {}, "ohne_zuordnung": true}
|
||||
"""
|
||||
let ergebnis = try JSONDecoder().decode(Abrufergebnis.self, from: Data(roh.utf8))
|
||||
#expect(ergebnis.ohneZuordnung == true)
|
||||
#expect(ergebnis.meldung != String(format: String(localized: "bank.abrufErgebnis"), 0, 0))
|
||||
}
|
||||
|
||||
@Test("Ein voller Abruf liest sich trotz der vielen Felder")
|
||||
func vollerAbruf() throws {
|
||||
let roh = """
|
||||
{"uebertraege": 1, "geholt": 42, "neu": 7, "gebucht": 5, "vorgemerkt": 2,
|
||||
"doppelt": 0, "saldi": {}, "ab": "2026-10-01", "ohne_zuordnung": false}
|
||||
"""
|
||||
let ergebnis = try JSONDecoder().decode(Abrufergebnis.self, from: Data(roh.utf8))
|
||||
#expect(ergebnis.geholt == 42 && ergebnis.neu == 7)
|
||||
#expect(ergebnis.uebertraege == 1)
|
||||
}
|
||||
}
|
||||
|
||||
extension ServerantwortenTests {
|
||||
|
||||
@Test("Erkennungswoerter, zweiter Faktor und Bankverbindungen")
|
||||
func nebenraeume() throws {
|
||||
let woerter = try lies([Erkennungswort].self, "schlagwoerter_der_kategorie")
|
||||
#expect(!woerter.isEmpty)
|
||||
#expect(woerter.allSatisfy { !$0.word.isEmpty })
|
||||
|
||||
// Leer, solange niemand oft genug korrigiert hat - der Typ muss sich
|
||||
// trotzdem lesen lassen.
|
||||
_ = try lies([MarkiertesWort].self, "markierteSchlagwoerter")
|
||||
|
||||
let faktor = try lies(ZweiterFaktorStand.self, "zweiterFaktor")
|
||||
#expect(faktor.enabled == false)
|
||||
|
||||
// Bleibt leer: Eine Anbindung laesst sich ohne echten Anbieter nicht
|
||||
// herstellen. Die Felder von Bankverbindung deckt dieser Test NICHT
|
||||
// ab - wer sie aendert, prueft von Hand.
|
||||
#expect(try lies([Bankverbindung].self, "bankverbindungen").isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user