SHA256
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.
89 lines
3.8 KiB
Swift
89 lines
3.8 KiB
Swift
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 {}
|
|
}
|
|
}
|