diff --git a/src/internal/http/DescopeClient.swift b/src/internal/http/DescopeClient.swift index 39a87fe..68d56fc 100644 --- a/src/internal/http/DescopeClient.swift +++ b/src/internal/http/DescopeClient.swift @@ -406,7 +406,7 @@ class DescopeClient: HTTPClient { var user: UserResponse? var firstSeen: Bool - mutating func setValues(from response: HTTPURLResponse) { + mutating func setValues(from data: Data, response: HTTPURLResponse) throws { guard let url = response.url, let fields = response.allHeaderFields as? [String: String] else { return } let cookies = HTTPCookie.cookies(withResponseHeaderFields: fields, for: url) for cookie in cookies where !cookie.value.isEmpty { @@ -421,71 +421,32 @@ class DescopeClient: HTTPClient { } struct UserResponse: JSONResponse { - var userId: String - var loginIds: [String] - var name: String? - var picture: String? - var email: String? - var verifiedEmail: Bool = false - var phone: String? - var verifiedPhone: Bool = false - var createdTime: Int - var givenName: String? - var middleName: String? - var familyName: String? - var customAttributes: [String: Any] = [:] - - enum CodingKeys: CodingKey { - case userId - case loginIds - case createdTime - case name - case picture - case email - case verifiedEmail - case phone - case verifiedPhone - case givenName - case middleName - case familyName - case customAttributes + // use a nested struct so we can let the compiler generate decoding for most members + struct UserFields: Decodable { + var userId: String + var loginIds: [String] + var createdTime: Int + var email: String? + var verifiedEmail: Bool? + var phone: String? + var verifiedPhone: Bool? + var name: String? + var givenName: String? + var middleName: String? + var familyName: String? + var picture: String? } - + + var userFields: UserFields + var customAttributes: [String: Any] = [:] + init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - userId = try values.decode(String.self, forKey: .userId) - loginIds = try values.decode(Array.self, forKey: .loginIds) - createdTime = try values.decode(Int.self, forKey: .createdTime) - if let value = try? values.decode(String?.self, forKey: .name) { - name = value - } - if let value = try? values.decode(String?.self, forKey: .picture) { - picture = value - } - if let value = try? values.decode(String?.self, forKey: .email) { - email = value - } - if let value = try? values.decode(Bool.self, forKey: .verifiedEmail) { - verifiedEmail = value - } - if let value = try? values.decode(String?.self, forKey: .phone) { - phone = value - } - if let value = try? values.decode(Bool?.self, forKey: .verifiedPhone) { - verifiedPhone = value - } - if let value = try? values.decode(String?.self, forKey: .givenName) { - givenName = value - } - if let value = try? values.decode(String?.self, forKey: .middleName) { - middleName = value - } - if let value = try? values.decode(String?.self, forKey: .familyName) { - familyName = value - } - if let value = try? values.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: .customAttributes) { - customAttributes = decodeJson(container: value) - } + userFields = try UserFields(from: decoder) + } + + mutating func setValues(from data: Data, response: HTTPURLResponse) throws { + let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:] + customAttributes = json["customAttributes"] as? [String: Any] ?? [:] } } diff --git a/src/internal/http/HTTPClient.swift b/src/internal/http/HTTPClient.swift index d7dec6d..69715d0 100644 --- a/src/internal/http/HTTPClient.swift +++ b/src/internal/http/HTTPClient.swift @@ -123,11 +123,11 @@ class HTTPClient { // JSON Response protocol JSONResponse: Decodable { - mutating func setValues(from response: HTTPURLResponse) + mutating func setValues(from data: Data, response: HTTPURLResponse) throws } extension JSONResponse { - mutating func setValues(from response: HTTPURLResponse) { + mutating func setValues(from data: Data, response: HTTPURLResponse) throws { // nothing by default } } @@ -135,7 +135,7 @@ extension JSONResponse { private func decodeJSON(data: Data, response: HTTPURLResponse) throws -> T { do { var val = try JSONDecoder().decode(T.self, from: data) - val.setValues(from: response) + try val.setValues(from: data, response: response) return val } catch { throw DescopeError.decodeError.with(cause: error) diff --git a/src/internal/others/Internal.swift b/src/internal/others/Internal.swift index af0f9eb..d6db17c 100644 --- a/src/internal/others/Internal.swift +++ b/src/internal/others/Internal.swift @@ -79,115 +79,3 @@ class AuthorizationDelegate: NSObject, ASAuthorizationControllerDelegate { completion = nil } } - -// JSON - -struct JSONCodingKeys: CodingKey { - var stringValue: String - var intValue: Int? - - init(stringValue: String) { - self.stringValue = stringValue - } - - init(intValue: Int) { - self.init(stringValue: "\(intValue)") - self.intValue = intValue - } -} - -func decodeJson(container: KeyedDecodingContainer) -> [String: Any] { - var decoded: [String: Any] = [:] - for key in container.allKeys { - if let boolValue = try? container.decode(Bool.self, forKey: key) { - decoded[key.stringValue] = boolValue - } else if let intValue = try? container.decode(Int.self, forKey: key) { - decoded[key.stringValue] = intValue - } else if let doubleValue = try? container.decode(Double.self, forKey: key) { - decoded[key.stringValue] = doubleValue - } else if let stringValue = try? container.decode(String.self, forKey: key) { - decoded[key.stringValue] = stringValue - } else if let nestedContainer = try? container.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) { - decoded[key.stringValue] = decodeJson(container: nestedContainer) - } else if var nestedUnkeyedContainer = try? container.nestedUnkeyedContainer(forKey: key) { - decoded[key.stringValue] = decodeJson(unkeydContainer: &nestedUnkeyedContainer) - } - } - return decoded -} - - -func decodeJson(unkeydContainer: inout UnkeyedDecodingContainer) -> [Any] { - var decoded: [Any] = [] - while unkeydContainer.isAtEnd == false { - if let value = try? unkeydContainer.decode(Bool.self) { - decoded.append(value) - } else if let value = try? unkeydContainer.decode(Int.self) { - decoded.append(value) - } else if let value = try? unkeydContainer.decode(Double.self) { - decoded.append(value) - } else if let value = try? unkeydContainer.decode(String.self) { - decoded.append(value) - } else if let _ = try? unkeydContainer.decode(String?.self) { - continue // Skip over `null` values - } else if let nestedContainer = try? unkeydContainer.nestedContainer(keyedBy: JSONCodingKeys.self) { - decoded.append(decodeJson(container: nestedContainer)) - } else if var nestedUnkeyedContainer = try? unkeydContainer.nestedUnkeyedContainer() { - decoded.append(decodeJson(unkeydContainer: &nestedUnkeyedContainer)) - } - } - return decoded -} - -extension Dictionary { - func encodeJson(container: inout KeyedEncodingContainer) throws { - try forEach({ (key, value) in - let encodingKey = JSONCodingKeys(stringValue: key) - switch value { - case let value as Bool: - try container.encode(value, forKey: encodingKey) - case let value as Int: - try container.encode(value, forKey: encodingKey) - case let value as Double: - try container.encode(value, forKey: encodingKey) - case let value as String: - try container.encode(value, forKey: encodingKey) - case let value as String?: - try container.encode(value, forKey: encodingKey) - case let value as [String: Any]: - var nestedContainer = container.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: encodingKey) - try value.encodeJson(container: &nestedContainer) - case let value as [Any]: - var nestedUnkeyedContainer = container.nestedUnkeyedContainer(forKey: encodingKey) - try value.encodeJson(container: &nestedUnkeyedContainer) - default: - throw DescopeError.encodeError.with(message: "Invalid JSON value in dict: \(key): \(value)") - } - }) - } -} - -extension Array { - func encodeJson(container: inout UnkeyedEncodingContainer) throws { - for value in self { - switch value { - case let value as Bool: - try container.encode(value) - case let value as Int: - try container.encode(value) - case let value as Double: - try container.encode(value) - case let value as String: - try container.encode(value) - case let value as [String: Any]: - var nestedContainer = container.nestedContainer(keyedBy: JSONCodingKeys.self) - try value.encodeJson(container: &nestedContainer) - case let value as [Any]: - var nestedUnkeyedContainer = container.nestedUnkeyedContainer() - try value.encodeJson(container: &nestedUnkeyedContainer) - default: - throw DescopeError.encodeError.with(message: "Invalid JSON value in array: \(value)") - } - } - } -} diff --git a/src/internal/routes/Shared.swift b/src/internal/routes/Shared.swift index 1756add..a5e4fc6 100644 --- a/src/internal/routes/Shared.swift +++ b/src/internal/routes/Shared.swift @@ -11,33 +11,40 @@ extension Route { } } -extension DescopeClient.UserResponse { +extension DescopeClient.UserResponse.UserFields { func convert() -> DescopeUser { let createdAt = Date(timeIntervalSince1970: TimeInterval(createdTime)) - var me = DescopeUser(userId: userId, loginIds: loginIds, createdAt: createdAt, isVerifiedEmail: false, isVerifiedPhone: false) + var user = DescopeUser(userId: userId, loginIds: loginIds, createdAt: createdAt, isVerifiedEmail: false, isVerifiedPhone: false) if let name, !name.isEmpty { - me.name = name - } - if let picture, let url = URL(string: picture) { - me.picture = url + user.name = name } if let email, !email.isEmpty { - me.email = email - me.isVerifiedEmail = verifiedEmail + user.email = email + user.isVerifiedEmail = verifiedEmail ?? false } if let phone, !phone.isEmpty { - me.phone = phone - me.isVerifiedPhone = verifiedPhone + user.phone = phone + user.isVerifiedPhone = verifiedPhone ?? false } if let givenName, !givenName.isEmpty { - me.givenName = givenName + user.givenName = givenName } if let middleName, !middleName.isEmpty { - me.middleName = middleName + user.middleName = middleName } if let familyName, !familyName.isEmpty { - me.familyName = familyName + user.familyName = familyName + } + if let picture, let url = URL(string: picture) { + user.picture = url } + return user + } +} + +extension DescopeClient.UserResponse { + func convert() -> DescopeUser { + var me = userFields.convert() me.customAttributes = customAttributes return me } diff --git a/src/types/User.swift b/src/types/User.swift index 9db90a6..96d3091 100644 --- a/src/types/User.swift +++ b/src/types/User.swift @@ -28,8 +28,8 @@ import Foundation /// In the code above we check that there's an active ``DescopeSession`` in the shared /// session manager. If so we ask the Descope server for the latest user details and /// then update the ``DescopeSession`` with them. -public struct DescopeUser: Codable, Equatable { - +public struct DescopeUser { + /// The unique identifier for the user in Descope. /// /// This value never changes after the user is created, and it always matches @@ -45,12 +45,6 @@ public struct DescopeUser: Codable, Equatable { /// The time at which the user was created in Descope. public var createdAt: Date - /// The user's full name. - public var name: String? - - /// The user's profile picture. - public var picture: URL? - /// The user's email address. /// /// If this is non-nil and the ``isVerifiedEmail`` flag is `true` then this email address @@ -71,6 +65,9 @@ public struct DescopeUser: Codable, Equatable { /// for this user. If ``phone`` is `nil` then this is always `false`. public var isVerifiedPhone: Bool + /// The user's full name. + public var name: String? + /// The user's given name. public var givenName: String? @@ -80,93 +77,28 @@ public struct DescopeUser: Codable, Equatable { /// The user's family name. public var familyName: String? - /// A mapping of any custom attributes associated with this user. - /// User custom attributes are managed via the Descope console. + /// The user's profile picture. + public var picture: URL? + + /// A mapping of any custom attributes associated with this user. The custom attributes + /// are managed via the Descope console. public var customAttributes: [String: Any] - public init(userId: String, loginIds: [String], createdAt: Date, name: String? = nil, picture: URL? = nil, email: String? = nil, isVerifiedEmail: Bool = false, phone: String? = nil, isVerifiedPhone: Bool = false, givenName: String? = nil, middleName: String? = nil, familyName: String? = nil, customAttributes: [String: Any] = [:]) { + public init(userId: String, loginIds: [String], createdAt: Date, email: String? = nil, isVerifiedEmail: Bool = false, phone: String? = nil, isVerifiedPhone: Bool = false, name: String? = nil, givenName: String? = nil, middleName: String? = nil, familyName: String? = nil, picture: URL? = nil, customAttributes: [String: Any] = [:]) { self.userId = userId self.loginIds = loginIds self.createdAt = createdAt - self.name = name - self.picture = picture self.email = email self.isVerifiedEmail = isVerifiedEmail self.phone = phone self.isVerifiedPhone = isVerifiedPhone + self.name = name self.givenName = givenName self.middleName = middleName self.familyName = familyName + self.picture = picture self.customAttributes = customAttributes } - - enum CodingKeys: CodingKey { - case userId - case loginIds - case createdAt - case name - case picture - case email - case isVerifiedEmail - case phone - case isVerifiedPhone - case givenName - case middleName - case familyName - case customAttributes - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - userId = try values.decode(String.self, forKey: .userId) - loginIds = try values.decode(Array.self, forKey: .loginIds) - createdAt = try values.decode(Date.self, forKey: .createdAt) - name = try values.decode(String?.self, forKey: .name) - picture = try values.decode(URL?.self, forKey: .picture) - email = try values.decode(String?.self, forKey: .email) - isVerifiedEmail = try values.decode(Bool.self, forKey: .isVerifiedEmail) - phone = try values.decode(String?.self, forKey: .phone) - isVerifiedPhone = try values.decode(Bool.self, forKey: .isVerifiedPhone) - givenName = try values.decode(String?.self, forKey: .givenName) - middleName = try values.decode(String?.self, forKey: .middleName) - familyName = try values.decode(String?.self, forKey: .familyName) - let value = try values.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: .customAttributes) - customAttributes = decodeJson(container: value) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(userId, forKey: .userId) - try container.encode(loginIds, forKey: .loginIds) - try container.encode(createdAt, forKey: .createdAt) - try container.encode(name, forKey: .name) - try container.encode(picture, forKey: .picture) - try container.encode(email, forKey: .email) - try container.encode(isVerifiedEmail, forKey: .isVerifiedEmail) - try container.encode(phone, forKey: .phone) - try container.encode(isVerifiedPhone, forKey: .isVerifiedPhone) - try container.encode(givenName, forKey: .givenName) - try container.encode(middleName, forKey: .middleName) - try container.encode(familyName, forKey: .familyName) - var nestedContainer = container.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: .customAttributes) - try customAttributes.encodeJson(container: &nestedContainer) - } - - public static func == (lhs: DescopeUser, rhs: DescopeUser) -> Bool { - return lhs.userId == rhs.userId && - lhs.loginIds == rhs.loginIds && - lhs.createdAt == rhs.createdAt && - lhs.name == rhs.name && - lhs.picture == rhs.picture && - lhs.email == rhs.email && - lhs.isVerifiedEmail == rhs.isVerifiedEmail && - lhs.phone == rhs.phone && - lhs.isVerifiedPhone == rhs.isVerifiedPhone && - lhs.givenName == rhs.givenName && - lhs.middleName == rhs.middleName && - lhs.familyName == rhs.familyName - // lhs.customAttributes == rhs.customAttributes - } } extension DescopeUser: CustomStringConvertible { @@ -184,3 +116,67 @@ extension DescopeUser: CustomStringConvertible { return "DescopeUser(id: \"\(userId)\"\(extras))" } } + +extension DescopeUser: Equatable { + public static func == (lhs: DescopeUser, rhs: DescopeUser) -> Bool { + let lhca = lhs.customAttributes as NSDictionary + let rhca = rhs.customAttributes as NSDictionary + return lhs.userId == rhs.userId && lhs.loginIds == rhs.loginIds && + lhs.createdAt == rhs.createdAt && lhs.picture == rhs.picture && + lhs.email == rhs.email && lhs.isVerifiedEmail == rhs.isVerifiedEmail && + lhs.phone == rhs.phone && lhs.isVerifiedPhone == rhs.isVerifiedPhone && + lhs.name == rhs.name && lhs.givenName == rhs.givenName && + lhs.middleName == rhs.middleName && lhs.familyName == rhs.familyName && + lhca.isEqual(to: rhca) + } +} + +// Unfortunately we can't rely on the compiler for automatic conformance to Codable because +// the customAttributes dictionary isn't serializable +extension DescopeUser: Codable { + enum CodingKeys: CodingKey { + case userId, loginIds, createdAt, email, isVerifiedEmail, phone, isVerifiedPhone, name, givenName, middleName, familyName, picture, customAttributes + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + userId = try values.decode(String.self, forKey: .userId) + loginIds = try values.decode([String].self, forKey: .loginIds) + createdAt = try values.decode(Date.self, forKey: .createdAt) + email = try values.decodeIfPresent(String.self, forKey: .email) + isVerifiedEmail = try values.decode(Bool.self, forKey: .isVerifiedEmail) + phone = try values.decodeIfPresent(String.self, forKey: .phone) + isVerifiedPhone = try values.decode(Bool.self, forKey: .isVerifiedPhone) + name = try values.decodeIfPresent(String.self, forKey: .name) + givenName = try values.decodeIfPresent(String.self, forKey: .givenName) + middleName = try values.decodeIfPresent(String.self, forKey: .middleName) + familyName = try values.decodeIfPresent(String.self, forKey: .familyName) + picture = try values.decodeIfPresent(URL.self, forKey: .picture) + if let data = try values.decodeIfPresent(Data.self, forKey: .customAttributes) { + let json = try JSONSerialization.jsonObject(with: data) + customAttributes = json as? [String: Any] ?? [:] + } else { + customAttributes = [:] + } + } + + public func encode(to encoder: Encoder) throws { + var values = encoder.container(keyedBy: CodingKeys.self) + try values.encode(userId, forKey: .userId) + try values.encode(loginIds, forKey: .loginIds) + try values.encode(createdAt, forKey: .createdAt) + try values.encodeIfPresent(email, forKey: .email) + try values.encode(isVerifiedEmail, forKey: .isVerifiedEmail) + try values.encodeIfPresent(phone, forKey: .phone) + try values.encode(isVerifiedPhone, forKey: .isVerifiedPhone) + try values.encodeIfPresent(name, forKey: .name) + try values.encodeIfPresent(givenName, forKey: .givenName) + try values.encodeIfPresent(middleName, forKey: .middleName) + try values.encodeIfPresent(familyName, forKey: .familyName) + try values.encodeIfPresent(picture, forKey: .picture) + // check before trying to serialize to prevent a runtime exception from being triggered + if JSONSerialization.isValidJSONObject(customAttributes), let data = try? JSONSerialization.data(withJSONObject: customAttributes) { + try values.encode(data, forKey: .customAttributes) + } + } +} diff --git a/test/http/DescopeClient.swift b/test/http/DescopeClient.swift deleted file mode 100644 index 0c6d927..0000000 --- a/test/http/DescopeClient.swift +++ /dev/null @@ -1,93 +0,0 @@ -import XCTest -@testable import DescopeKit - -class TestResponses: XCTestCase { - func testUserResponse() throws { - let jsonString = """ - { - "userId": "userId", - "loginIds": ["loginId"], - "name": "name", - "picture": "picture", - "email": "email", - "verifiedEmail": true, - "phone": "phone", - "createdTime": 123, - "middleName": "middleName", - "familyName": "familyName", - "customAttributes": { - "a": "yes", - "b": true, - "c": 1, - "d": null, - "unnecessaryArray": [ - "yes", - true, - null, - 1, - { - "a": "yes", - "b": true, - "c": 1, - "d": null - } - ] - } - } - """ - guard let data = jsonString.data(using: .utf8) else { return XCTFail("Couldn't get data from json string")} - guard let userResponse = try? JSONDecoder().decode(DescopeClient.UserResponse.self, from: data) else { return XCTFail("Couldn't decode") } - XCTAssertEqual("userId", userResponse.userId) - XCTAssertFalse(userResponse.verifiedPhone) - XCTAssertNil(userResponse.givenName) - - // customAttributes - try checkDictionary(userResponse.customAttributes) - - // customAttributes.unnecessaryArray - guard let array = userResponse.customAttributes["unnecessaryArray"] as? Array else { return XCTFail("Couldn't get custom attirubte value as array") } - XCTAssertEqual(4, array.count) // null value omitted - try checkArray(array) - - // customAttributes.unnecessaryArray[3] - guard let dict = array[3] as? [String: Any] else { return XCTFail("Couldn't get custom attirubte value as array") } - try checkDictionary(dict) - - // convert to DescopeUser and check again - let user = userResponse.convert() - XCTAssertEqual("userId", user.userId) - XCTAssertFalse(user.isVerifiedPhone) - XCTAssertNil(user.givenName) - - // customAttributes - try checkDictionary(user.customAttributes) - - // customAttributes.unnecessaryArray - guard let array = user.customAttributes["unnecessaryArray"] as? Array else { return XCTFail("Couldn't get custom attirubte value as array") } - XCTAssertEqual(4, array.count) // null value omitted - try checkArray(array) - - // customAttributes.unnecessaryArray[3] - guard let dict = array[3] as? [String: Any] else { return XCTFail("Couldn't get custom attirubte value as array") } - try checkDictionary(dict) - } - - func checkDictionary(_ dict: [String:Any]) throws { - guard let aValue = dict["a"] as? String else { return XCTFail("Couldn't get custom attirubte value as String") } - XCTAssertEqual("yes", aValue) - guard let bValue = dict["b"] as? Bool else { return XCTFail("Couldn't get custom attirubte value as Bool") } - XCTAssertTrue(bValue) - guard let cValue = dict["c"] as? Int else { return XCTFail("Couldn't get custom attirubte value as Int") } - XCTAssertEqual(1, cValue) - XCTAssertNil(dict["d"]) - } - - func checkArray(_ array: [Any]) throws { - guard let aValue = array[0] as? String else { return XCTFail("Couldn't get custom attirubte value as string") } - XCTAssertEqual("yes", aValue) - guard let bValue = array[1] as? Bool else { return XCTFail("Couldn't get custom attirubte value as bool") } - XCTAssertTrue(bValue) - guard let cValue = array[2] as? Int else { return XCTFail("Couldn't get custom attirubte value as int") } - XCTAssertEqual(1, cValue) - } -} diff --git a/test/http/HTTPClient.swift b/test/http/HTTPClient.swift index d68f9f6..8094705 100644 --- a/test/http/HTTPClient.swift +++ b/test/http/HTTPClient.swift @@ -97,7 +97,7 @@ private struct MockResponse: JSONResponse, Equatable { static let json: [String: Any] = ["id": instance.id, "st": instance.st] static let headers: [String: String] = ["hd": instance.hd!] - mutating func setValues(from response: HTTPURLResponse) { + mutating func setValues(from data: Data, response: HTTPURLResponse) throws { guard let headers = response.allHeaderFields as? [String: String] else { return } for (name, value) in headers where name == "hd" { hd = value diff --git a/test/mocks/MockDescope.swift b/test/mocks/MockDescope.swift new file mode 100644 index 0000000..1038a4d --- /dev/null +++ b/test/mocks/MockDescope.swift @@ -0,0 +1,10 @@ +import Foundation +@testable import DescopeKit + +extension DescopeSDK { + static func mock(projectId: String = "projId") -> DescopeSDK { + var config = DescopeConfig(projectId: projectId, logger: DescopeLogger()) + config.networkClient = MockHTTP.networkClient + return DescopeSDK(config: config) + } +} diff --git a/test/mocks/MockHTTP.swift b/test/mocks/MockHTTP.swift index 52e28cc..62bc3a4 100644 --- a/test/mocks/MockHTTP.swift +++ b/test/mocks/MockHTTP.swift @@ -57,6 +57,10 @@ extension MockHTTP { responses.append((statusCode, nil, nil, error, validator)) } + static func push(statusCode: Int = 200, body: String, headers: [String: String]? = nil, validator: RequestValidator? = nil) { + responses.append((statusCode, Data(body.utf8), headers, nil, validator)) + } + static func push(statusCode: Int = 200, json: [String: Any], headers: [String: String]? = nil, validator: RequestValidator? = nil) { guard let data = try? JSONSerialization.data(withJSONObject: json) else { preconditionFailure("Failed to serialize JSON") } responses.append((statusCode, data, headers, nil, validator)) diff --git a/test/routes/AccessKey.swift b/test/routes/AccessKey.swift index 92987bc..1728318 100644 --- a/test/routes/AccessKey.swift +++ b/test/routes/AccessKey.swift @@ -6,17 +6,15 @@ private let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiYXIiLCJuYW1l class TestAccessKey: XCTestCase { func testTokenDecoding() async throws { - var config = DescopeConfig(projectId: "foo") - config.networkClient = MockHTTP.networkClient - let descope = DescopeSDK(config: config) - + let descope = DescopeSDK.mock() + MockHTTP.push(json: ["sessionJwt": jwt]) { request in XCTAssertEqual(request.httpMethod, "POST") - XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer foo:bar") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer projId:key") XCTAssertEqual(request.httpBody, Data("{}".utf8)) } - let token = try await descope.accessKey.exchange(accessKey: "bar") + let token = try await descope.accessKey.exchange(accessKey: "key") XCTAssertEqual(jwt, token.jwt) XCTAssertEqual("bar", token.id) XCTAssertEqual("foo", token.projectId) diff --git a/test/routes/Auth.swift b/test/routes/Auth.swift new file mode 100644 index 0000000..1da9cd3 --- /dev/null +++ b/test/routes/Auth.swift @@ -0,0 +1,73 @@ +import XCTest +@testable import DescopeKit + +class TestAuth: XCTestCase { + func testMe() async throws { + let descope = DescopeSDK.mock() + + MockHTTP.push(body: mePayload) { request in + XCTAssertEqual(request.httpMethod, "GET") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer projId:jwt") + } + + let user = try await descope.auth.me(refreshJwt: "jwt") + XCTAssertEqual("userId", user.userId) + XCTAssertFalse(user.isVerifiedPhone) + XCTAssertTrue(user.isVerifiedEmail) + XCTAssertNil(user.givenName) + + // customAttributes + try checkDictionary(user.customAttributes) + + // customAttributes.unnecessaryArray + guard let array = user.customAttributes["unnecessaryArray"] as? [Any] else { return XCTFail() } + try checkArray(array) + + // customAttributes.unnecessaryArray[3] + guard let dict = array[3] as? [String: Any] else { return XCTFail() } + try checkDictionary(dict) + } + + func checkDictionary(_ dict: [String: Any]) throws { + XCTAssertEqual("yes", dict["a"] as? String) + XCTAssertEqual(true, dict["b"] as? Bool) + XCTAssertEqual(1, dict["c"] as? Int) + } + + func checkArray(_ array: [Any]) throws { + XCTAssertEqual("yes", array[0] as? String) + XCTAssertEqual(true, array[1] as? Bool) + XCTAssertEqual(1, array[2] as? Int) + } +} + +private let mePayload = """ +{ + "userId": "userId", + "loginIds": ["loginId"], + "name": "name", + "picture": "picture", + "email": "email", + "verifiedEmail": true, + "phone": "phone", + "createdTime": 123, + "middleName": "middleName", + "familyName": "familyName", + "customAttributes": { + "a": "yes", + "b": true, + "c": 1, + "d": null, + "unnecessaryArray": [ + "yes", + true, + 1, + { + "a": "yes", + "b": true, + "c": 1, + } + ] + } +} +""" diff --git a/test/types/User.swift b/test/types/User.swift index bd47526..f5faca2 100644 --- a/test/types/User.swift +++ b/test/types/User.swift @@ -12,5 +12,4 @@ class TestUser: XCTestCase { guard let aValue = decodedUser.customAttributes["a"] as? String else { return XCTFail("Couldn't get custom attirubte value as String") } XCTAssertEqual("yes", aValue) } - }