import XCTest @testable import Yuvomi final class NativeContractsTests: XCTestCase { private func decode(_ 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 {} } }