SHA256
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:
1 parent
11bf4ca620
commit
97570b7fdf
15 files changed
+1182
-11
No files matched your search
@@ -1,40 +0,0 @@
|
||||
import WebKit
|
||||
import XCTest
|
||||
|
||||
@testable import Yuvomi
|
||||
|
||||
final class BrowserModelTests: XCTestCase {
|
||||
@MainActor
|
||||
func testMainViewHasNoNativeBridgeOrAutomaticPopupAccess() {
|
||||
let model = BrowserModel()
|
||||
XCTAssertFalse(model.web.configuration.preferences.javaScriptCanOpenWindowsAutomatically)
|
||||
XCTAssertTrue(model.web.configuration.userContentController.userScripts.isEmpty)
|
||||
XCTAssertTrue(model.web.configuration.websiteDataStore.isPersistent)
|
||||
XCTAssertFalse(model.web.isInspectable)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLateExportCancellationDoesNotEraseAnotherServersDownload() {
|
||||
let model = BrowserModel()
|
||||
let previous = URL(fileURLWithPath: "/old-selection.txt")
|
||||
let current = URL(fileURLWithPath: "/new-selection.txt")
|
||||
model.download = WebDestination(url: current)
|
||||
model.removeDownload(expected: previous)
|
||||
XCTAssertEqual(model.download?.url, current)
|
||||
model.removeDownload(expected: current)
|
||||
XCTAssertNil(model.download)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testServerErasureReplacesHistoryContext() async {
|
||||
let model = BrowserModel()
|
||||
let previousView = model.web
|
||||
await withCheckedContinuation { continuation in
|
||||
model.erase { continuation.resume() }
|
||||
}
|
||||
XCTAssertFalse(previousView === model.web)
|
||||
XCTAssertNil(model.server)
|
||||
XCTAssertFalse(model.canGoBack)
|
||||
XCTAssertNil(model.error)
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user