From ff5b5c0eac2d06a52ac0135bb39333a053132e9a Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 6 Mar 2025 13:39:38 +1100
Subject: [PATCH 1/2] fix: handle missing user `theme_preference` on sign in

---
 Coder Desktop/CoderSDK/Client.swift     | 27 +++++++++++++---
 Coder Desktop/CoderSDK/Deployment.swift |  6 +---
 Coder Desktop/CoderSDK/User.swift       | 41 ++-----------------------
 3 files changed, 25 insertions(+), 49 deletions(-)

diff --git a/Coder Desktop/CoderSDK/Client.swift b/Coder Desktop/CoderSDK/Client.swift
index 43e9b59..85bc8f3 100644
--- a/Coder Desktop/CoderSDK/Client.swift	
+++ b/Coder Desktop/CoderSDK/Client.swift	
@@ -44,7 +44,7 @@ public struct Client {
             throw .network(error)
         }
         guard let httpResponse = resp as? HTTPURLResponse else {
-            throw .unexpectedResponse(data)
+            throw .unexpectedResponse(String(data: data, encoding: .utf8) ?? "<non-utf8 data>")
         }
         return HTTPResponse(resp: httpResponse, data: data, req: req)
     }
@@ -72,7 +72,7 @@ public struct Client {
 
     func responseAsError(_ resp: HTTPResponse) -> ClientError {
         do {
-            let body = try Client.decoder.decode(Response.self, from: resp.data)
+            let body = try decode(Response.self, from: resp.data)
             let out = APIError(
                 response: body,
                 statusCode: resp.resp.statusCode,
@@ -81,7 +81,24 @@ public struct Client {
             )
             return .api(out)
         } catch {
-            return .unexpectedResponse(resp.data.prefix(1024))
+            return .unexpectedResponse(String(data: resp.data, encoding: .utf8) ?? "<non-utf8 data>")
+        }
+    }
+
+    // Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`.
+    func decode<T>(_: T.Type, from data: Data) throws(ClientError) -> T where T: Decodable {
+        do {
+            return try Client.decoder.decode(T.self, from: data)
+        } catch let DecodingError.keyNotFound(_, context) {
+            throw .unexpectedResponse("Key not found: \(context.debugDescription)")
+        } catch let DecodingError.valueNotFound(_, context) {
+            throw .unexpectedResponse("Value not found: \(context.debugDescription)")
+        } catch let DecodingError.typeMismatch(_, context) {
+            throw .unexpectedResponse("Type mismatch: \(context.debugDescription)")
+        } catch let DecodingError.dataCorrupted(context) {
+            throw .unexpectedResponse("Data corrupted: \(context.debugDescription)")
+        } catch {
+            throw .unexpectedResponse(String(data: data.prefix(1024), encoding: .utf8) ?? "<non-utf8 data>")
         }
     }
 }
@@ -119,7 +136,7 @@ public struct FieldValidation: Decodable, Sendable {
 public enum ClientError: Error {
     case api(APIError)
     case network(any Error)
-    case unexpectedResponse(Data)
+    case unexpectedResponse(String)
     case encodeFailure(any Error)
 
     public var description: String {
@@ -129,7 +146,7 @@ public enum ClientError: Error {
         case let .network(error):
             error.localizedDescription
         case let .unexpectedResponse(data):
-            "Unexpected or non HTTP response: \(data)"
+            "Unexpected response: \(data)"
         case let .encodeFailure(error):
             "Failed to encode body: \(error.localizedDescription)"
         }
diff --git a/Coder Desktop/CoderSDK/Deployment.swift b/Coder Desktop/CoderSDK/Deployment.swift
index 8144c0a..3218a6f 100644
--- a/Coder Desktop/CoderSDK/Deployment.swift	
+++ b/Coder Desktop/CoderSDK/Deployment.swift	
@@ -6,11 +6,7 @@ public extension Client {
         guard res.resp.statusCode == 200 else {
             throw responseAsError(res)
         }
-        do {
-            return try Client.decoder.decode(BuildInfoResponse.self, from: res.data)
-        } catch {
-            throw .unexpectedResponse(res.data.prefix(1024))
-        }
+        return try decode(BuildInfoResponse.self, from: res.data)
     }
 }
 
diff --git a/Coder Desktop/CoderSDK/User.swift b/Coder Desktop/CoderSDK/User.swift
index e7f85f4..ad81cf0 100644
--- a/Coder Desktop/CoderSDK/User.swift	
+++ b/Coder Desktop/CoderSDK/User.swift	
@@ -6,57 +6,20 @@ public extension Client {
         guard res.resp.statusCode == 200 else {
             throw responseAsError(res)
         }
-        do {
-            return try Client.decoder.decode(User.self, from: res.data)
-        } catch {
-            throw .unexpectedResponse(res.data.prefix(1024))
-        }
+        return try decode(User.self, from: res.data)
     }
 }
 
 public struct User: Encodable, Decodable, Equatable, Sendable {
     public let id: UUID
     public let username: String
-    public let avatar_url: String
-    public let name: String
-    public let email: String
-    public let created_at: Date
-    public let updated_at: Date
-    public let last_seen_at: Date
-    public let status: String
-    public let login_type: String
-    public let theme_preference: String
-    public let organization_ids: [UUID]
-    public let roles: [Role]
 
     public init(
         id: UUID,
-        username: String,
-        avatar_url: String,
-        name: String,
-        email: String,
-        created_at: Date,
-        updated_at: Date,
-        last_seen_at: Date,
-        status: String,
-        login_type: String,
-        theme_preference: String,
-        organization_ids: [UUID],
-        roles: [Role]
+        username: String
     ) {
         self.id = id
         self.username = username
-        self.avatar_url = avatar_url
-        self.name = name
-        self.email = email
-        self.created_at = created_at
-        self.updated_at = updated_at
-        self.last_seen_at = last_seen_at
-        self.status = status
-        self.login_type = login_type
-        self.theme_preference = theme_preference
-        self.organization_ids = organization_ids
-        self.roles = roles
     }
 }
 

From 7243289fe5b8c8ea91f3dbd386f32d1fbc70d36d Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 6 Mar 2025 14:07:22 +1100
Subject: [PATCH 2/2] remove unused fields from other structs

---
 .../Coder DesktopTests/LoginFormTests.swift   | 13 +---------
 Coder Desktop/CoderSDK/Deployment.swift       | 10 +------
 Coder Desktop/CoderSDK/User.swift             | 12 ---------
 .../CoderSDKTests/CoderSDKTests.swift         | 26 ++-----------------
 4 files changed, 4 insertions(+), 57 deletions(-)

diff --git a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift b/Coder Desktop/Coder DesktopTests/LoginFormTests.swift
index e966178..b58f817 100644
--- a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift	
+++ b/Coder Desktop/Coder DesktopTests/LoginFormTests.swift	
@@ -93,18 +93,7 @@ struct LoginTests {
 
         let user = User(
             id: UUID(),
-            username: "admin",
-            avatar_url: "",
-            name: "admin",
-            email: "admin@coder.com",
-            created_at: Date.now,
-            updated_at: Date.now,
-            last_seen_at: Date.now,
-            status: "active",
-            login_type: "none",
-            theme_preference: "dark",
-            organization_ids: [],
-            roles: []
+            username: "admin"
         )
 
         try Mock(
diff --git a/Coder Desktop/CoderSDK/Deployment.swift b/Coder Desktop/CoderSDK/Deployment.swift
index 3218a6f..8357a7e 100644
--- a/Coder Desktop/CoderSDK/Deployment.swift	
+++ b/Coder Desktop/CoderSDK/Deployment.swift	
@@ -10,16 +10,8 @@ public extension Client {
     }
 }
 
-public struct BuildInfoResponse: Encodable, Decodable, Equatable, Sendable {
-    public let external_url: String
+public struct BuildInfoResponse: Codable, Equatable, Sendable {
     public let version: String
-    public let dashboard_url: String
-    public let telemetry: Bool
-    public let workspace_proxy: Bool
-    public let agent_api_version: String
-    public let provisioner_api_version: String
-    public let upgrade_message: String
-    public let deployment_id: String
 
     // `version` in the form `[0-9]+.[0-9]+.[0-9]+`
     public var semver: String? {
diff --git a/Coder Desktop/CoderSDK/User.swift b/Coder Desktop/CoderSDK/User.swift
index ad81cf0..ca1bbf7 100644
--- a/Coder Desktop/CoderSDK/User.swift	
+++ b/Coder Desktop/CoderSDK/User.swift	
@@ -22,15 +22,3 @@ public struct User: Encodable, Decodable, Equatable, Sendable {
         self.username = username
     }
 }
-
-public struct Role: Encodable, Decodable, Equatable, Sendable {
-    public let name: String
-    public let display_name: String
-    public let organization_id: UUID?
-
-    public init(name: String, display_name: String, organization_id: UUID?) {
-        self.name = name
-        self.display_name = display_name
-        self.organization_id = organization_id
-    }
-}
diff --git a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift
index 8184730..e7675b7 100644
--- a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift	
+++ b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift	
@@ -7,23 +7,9 @@ import Testing
 struct CoderSDKTests {
     @Test
     func user() async throws {
-        let now = Date.now
         let user = User(
             id: UUID(),
-            username: "johndoe",
-            avatar_url: "https://example.com/img.png",
-            name: "John Doe",
-            email: "john.doe@example.com",
-            created_at: now,
-            updated_at: now,
-            last_seen_at: now,
-            status: "active",
-            login_type: "email",
-            theme_preference: "dark",
-            organization_ids: [UUID()],
-            roles: [
-                Role(name: "user", display_name: "User", organization_id: UUID()),
-            ]
+            username: "johndoe"
         )
 
         let url = URL(string: "https://example.com")!
@@ -50,15 +36,7 @@ struct CoderSDKTests {
     @Test
     func buildInfo() async throws {
         let buildInfo = BuildInfoResponse(
-            external_url: "https://example.com",
-            version: "v2.18.2-devel+630fd7c0a",
-            dashboard_url: "https://example.com/dashboard",
-            telemetry: true,
-            workspace_proxy: false,
-            agent_api_version: "1.0",
-            provisioner_api_version: "1.2",
-            upgrade_message: "foo",
-            deployment_id: UUID().uuidString
+            version: "v2.18.2-devel+630fd7c0a"
         )
 
         let url = URL(string: "https://example.com")!