feat(ios): replace embedded website with initial native SwiftUI API client

Introduce real SwiftUI auth/dashboard/task/note/shopping screens, a bounded origin-bound URLSession transport and Keychain-backed native sessions. Archive the rejected WebKit renderer rather than embedding it for unfinished modules. Sixteen portable URL/contract/transport tests and Swift format/parsing pass on Linux; Apple SDK application and UI validation remains mandatory. This is an incomplete native slice, not full PWA parity or a release.
This commit is contained in:
Yuvomi Mobile Contributors committed 2026-10-21 08:57:31 +02:00
1 parent 11bf4ca620
commit 97570b7fdf
15 files changed
+1182 -11

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
import XCTest
@testable import Yuvomi
final class NativeContractsTests: XCTestCase {
private func decode<T: Decodable>(_ type: T.Type, _ json: String) throws -> T {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(type, from: Data(json.utf8))
}
func testSparsePermissionsPreserveReadAndHiddenStates() throws {
let permissions = try decode(
NativePermissions.self,
#"{"admin":false,"modules":{"tasks":"read","notes":"none"},"widgets":{"shopping":"none"}}"#)
XCTAssertEqual(permissions.access("tasks"), "read")
XCTAssertEqual(permissions.access("notes"), "none")
XCTAssertEqual(permissions.access("shopping"), "write")
XCTAssertFalse(permissions.showsWidget("shopping"))
}
func testTwoFactorResponseDoesNotInventSignedInUser() throws {
let identity = try decode(NativeIdentity.self, #"{"twoFactorRequired":true,"recoveryAvailable":true}"#)
XCTAssertEqual(identity.twoFactorRequired, true)
XCTAssertNil(identity.user)
XCTAssertNil(identity.permissions)
}
func testTaskFieldsMatchActualServerContract() throws {
let task = try decode(
NativeTask.self,
#"{"id":17,"title":"Native task","description":null,"status":"open","priority":"high","due_date":"2026-10-21","category":"general","recurrence_rule":"FREQ=DAILY"}"#
)
XCTAssertEqual(task.id, 17)
XCTAssertEqual(task.dueDate, "2026-10-21")
XCTAssertNil(task.description)
}
func testShoppingUsesServerCounterNames() throws {
let list = try decode(NativeShoppingList.self, #"{"id":3,"name":"Einkauf","item_total":8,"item_checked":2}"#)
XCTAssertEqual(list.itemTotal, 8)
XCTAssertEqual(list.itemChecked, 2)
XCTAssertNil(list.openCount)
}
func testDashboardUnavailableCountsStayUnknown() throws {
let dashboard = try decode(
NativeDashboard.self,
#"{"openTaskCount":null,"shoppingOpenCount":0,"urgentTasks":[],"upcomingEvents":[],"shoppingLists":[],"pinnedNotes":[]}"#
)
XCTAssertNil(dashboard.openTaskCount)
XCTAssertEqual(dashboard.shoppingOpenCount, 0)
}
func testMutationEncodingPreservesNullAndNumericFlags() throws {
let body: [String: NativeValue] = ["due_date": .null, "is_checked": .number(1), "title": .string("Native")]
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: JSONEncoder().encode(body)) as? [String: Any])
XCTAssertTrue(json["due_date"] is NSNull)
XCTAssertEqual(json["is_checked"] as? Int, 1)
XCTAssertEqual(json["title"] as? String, "Native")
}
func testRejectsBooleanWhereServerUsesIntegerState() {
XCTAssertThrowsError(try decode(NativeShoppingItem.self, #"{"id":1,"name":"Apfel","is_checked":true}"#))
}
@MainActor
func testNativeTransportRejectsNonAPIPathsBeforeNetwork() async throws {
let api = try NativeAPI(server: ServerAddress("https://example.invalid"), saved: nil, persist: { _ in })
for path in ["https://evil.invalid", "//evil.invalid", "/path#fragment", "/path\\escape"] {
do {
let _: NativeEmpty = try await api.request(path, as: NativeEmpty.self)
XCTFail("Invalid path accepted")
} catch NativeAPIError.invalidPath {}
}
await api.close()
}
@MainActor
func testClosedTransportCannotReuseAnOldSession() async throws {
let api = try NativeAPI(server: ServerAddress("https://example.invalid"), saved: nil, persist: { _ in })
await api.close()
do {
let _: NativeEmpty = try await api.request("/auth/me", as: NativeEmpty.self)
XCTFail("Closed session accepted a request")
} catch NativeAPIError.closed {}
}
}