Skip to content

Commit 5aeb803

Browse files
authored
docs: improve docs for AuthError.APIError (#191)
1 parent ec07c95 commit 5aeb803

File tree

8 files changed

+83
-14
lines changed

8 files changed

+83
-14
lines changed

Sources/Auth/AuthClient.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public actor AuthClient {
7171
Dependencies.current.value!.eventEmitter
7272
}
7373

74+
private var currentDate: @Sendable () -> Date {
75+
Dependencies.current.value!.currentDate
76+
}
77+
7478
/// Returns the session, refreshing it if necessary.
7579
///
7680
/// If no session can be found, a ``AuthError/sessionNotFound`` error is thrown.
@@ -496,13 +500,16 @@ public actor AuthClient {
496500

497501
guard
498502
let accessToken = params.first(where: { $0.name == "access_token" })?.value,
499-
let expiresIn = params.first(where: { $0.name == "expires_in" })?.value,
503+
let expiresIn = params.first(where: { $0.name == "expires_in" }).map(\.value)
504+
.flatMap(TimeInterval.init),
500505
let refreshToken = params.first(where: { $0.name == "refresh_token" })?.value,
501506
let tokenType = params.first(where: { $0.name == "token_type" })?.value
502507
else {
503508
throw URLError(.badURL)
504509
}
505510

511+
let expiresAt = params.first(where: { $0.name == "expires_at" }).map(\.value)
512+
.flatMap(TimeInterval.init)
506513
let providerToken = params.first(where: { $0.name == "provider_token" })?.value
507514
let providerRefreshToken = params.first(where: { $0.name == "provider_refresh_token" })?.value
508515

@@ -519,7 +526,8 @@ public actor AuthClient {
519526
providerRefreshToken: providerRefreshToken,
520527
accessToken: accessToken,
521528
tokenType: tokenType,
522-
expiresIn: Double(expiresIn) ?? 0,
529+
expiresIn: expiresIn,
530+
expiresAt: expiresAt ?? currentDate().addingTimeInterval(expiresIn).timeIntervalSince1970,
523531
refreshToken: refreshToken,
524532
user: user
525533
)
@@ -545,7 +553,7 @@ public actor AuthClient {
545553
/// - Returns: A new valid session.
546554
@discardableResult
547555
public func setSession(accessToken: String, refreshToken: String) async throws -> Session {
548-
let now = Date()
556+
let now = currentDate()
549557
var expiresAt = now
550558
var hasExpired = true
551559
var session: Session
@@ -566,6 +574,7 @@ public actor AuthClient {
566574
accessToken: accessToken,
567575
tokenType: "bearer",
568576
expiresIn: expiresAt.timeIntervalSince(now),
577+
expiresAt: expiresAt.timeIntervalSince1970,
569578
refreshToken: refreshToken,
570579
user: user
571580
)

Sources/Auth/AuthError.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,31 @@ public enum AuthError: LocalizedError, Sendable {
99
case invalidImplicitGrantFlowURL
1010

1111
public struct APIError: Error, Decodable, Sendable {
12-
public var message: String?
12+
/// A basic message describing the problem with the request. Usually missing if
13+
/// ``AuthError/APIError/error`` is present.
1314
public var msg: String?
15+
16+
/// The HTTP status code. Usually missing if ``AuthError/APIError/error`` is present.
1417
public var code: Int?
18+
19+
/// Certain responses will contain this property with the provided values.
20+
///
21+
/// Usually one of these:
22+
/// - `invalid_request`
23+
/// - `unauthorized_client`
24+
/// - `access_denied`
25+
/// - `server_error`
26+
/// - `temporarily_unavailable`
27+
/// - `unsupported_otp_type`
1528
public var error: String?
29+
30+
/// Certain responses that have an ``AuthError/APIError/error`` property may have this property
31+
/// which describes the error.
1632
public var errorDescription: String?
33+
34+
/// Only returned when signing up if the password used is too weak. Inspect the
35+
/// ``WeakPassword/reasons`` and ``AuthError/APIError/msg`` property to identify the causes.
36+
public var weakPassword: WeakPassword?
1737
}
1838

1939
public enum PKCEFailureReason: Sendable {
@@ -23,10 +43,10 @@ public enum AuthError: LocalizedError, Sendable {
2343

2444
public var errorDescription: String? {
2545
switch self {
46+
case let .api(error): return error.errorDescription ?? error.msg ?? error.error
2647
case .missingExpClaim: return "Missing expiration claim on access token."
2748
case .malformedJWT: return "A malformed JWT received."
2849
case .sessionNotFound: return "Unable to get a valid session."
29-
case let .api(error): return error.errorDescription ?? error.message ?? error.msg
3050
case .pkce(.codeVerifierNotFound): return "A code verifier wasn't found in PKCE flow."
3151
case .pkce(.invalidPKCEFlowURL): return "Not a valid PKCE flow url."
3252
case .invalidImplicitGrantFlowURL:

Sources/Auth/Internal/Dependencies.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ struct Dependencies: Sendable {
1111
var sessionStorage: SessionStorage
1212
var sessionRefresher: SessionRefresher
1313
var codeVerifierStorage: CodeVerifierStorage
14+
var currentDate: @Sendable () -> Date = { Date() }
1415
}

Sources/Auth/Types.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,43 +47,64 @@ public struct Session: Codable, Hashable, Sendable {
4747
/// The oauth provider token. If present, this can be used to make external API requests to the
4848
/// oauth provider used.
4949
public var providerToken: String?
50+
5051
/// The oauth provider refresh token. If present, this can be used to refresh the provider_token
5152
/// via the oauth provider's API. Not all oauth providers return a provider refresh token. If the
5253
/// provider_refresh_token is missing, please refer to the oauth provider's documentation for
5354
/// information on how to obtain the provider refresh token.
5455
public var providerRefreshToken: String?
55-
/// The access token jwt. It is recommended to set the JWT_EXPIRY to a shorter expiry value.
56+
57+
/// A valid JWT that will expire in ``Session/expiresIn`` seconds.
58+
/// It is recommended to set the `JWT_EXPIRY` to a shorter expiry value.
5659
public var accessToken: String
60+
61+
/// What type of token this is. Only `bearer` returned, may change in the future.
5762
public var tokenType: String
58-
/// The number of seconds until the token expires (since it was issued). Returned when a login is
59-
/// confirmed.
60-
public var expiresIn: Double
61-
/// A one-time used refresh token that never expires.
63+
64+
/// Number of seconds after which the ``Session/accessToken`` should be renewed by using the
65+
/// refresh token with the `refresh_token` grant type.
66+
public var expiresIn: TimeInterval
67+
68+
/// UNIX timestamp after which the ``Session/accessToken`` should be renewed by using the refresh
69+
/// token with the `refresh_token` grant type.
70+
public var expiresAt: TimeInterval?
71+
72+
/// An opaque string that can be used once to obtain a new access and refresh token.
6273
public var refreshToken: String
74+
75+
/// Only returned on the `/token?grant_type=password` endpoint. When present, it indicates that
76+
/// the password used is weak. Inspect the ``WeakPassword/reasons`` property to identify why.
77+
public var weakPassword: WeakPassword?
78+
6379
public var user: User
6480

6581
public init(
6682
providerToken: String? = nil,
6783
providerRefreshToken: String? = nil,
6884
accessToken: String,
6985
tokenType: String,
70-
expiresIn: Double,
86+
expiresIn: TimeInterval,
87+
expiresAt: TimeInterval?,
7188
refreshToken: String,
89+
weakPassword: WeakPassword? = nil,
7290
user: User
7391
) {
7492
self.providerToken = providerToken
7593
self.providerRefreshToken = providerRefreshToken
7694
self.accessToken = accessToken
7795
self.tokenType = tokenType
7896
self.expiresIn = expiresIn
97+
self.expiresAt = expiresAt
7998
self.refreshToken = refreshToken
99+
self.weakPassword = weakPassword
80100
self.user = user
81101
}
82102

83103
static let empty = Session(
84104
accessToken: "",
85105
tokenType: "",
86106
expiresIn: 0,
107+
expiresAt: nil,
87108
refreshToken: "",
88109
user: User(
89110
id: UUID(),
@@ -618,3 +639,9 @@ public struct ResendMobileResponse: Decodable, Hashable, Sendable {
618639
self.messageId = messageId
619640
}
620641
}
642+
643+
public struct WeakPassword: Codable, Hashable, Sendable {
644+
/// List of reasons the password is too weak, could be any of `length`, `characters`, or
645+
/// `pwned`.
646+
public let reasons: [String]
647+
}

Tests/AuthTests/AuthResponseTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import XCTest
44

55
final class AuthResponseTests: XCTestCase {
66
func testSession() throws {
7-
let response = try JSONDecoder.goTrue.decode(AuthResponse.self, from: json(named: "session"))
7+
let response = try AuthClient.Configuration.jsonDecoder.decode(
8+
AuthResponse.self,
9+
from: json(named: "session")
10+
)
811
XCTAssertNotNil(response.session)
912
XCTAssertEqual(response.user, response.session?.user)
1013
}
1114

1215
func testUser() throws {
13-
let response = try JSONDecoder.goTrue.decode(AuthResponse.self, from: json(named: "user"))
16+
let response = try AuthClient.Configuration.jsonDecoder.decode(
17+
AuthResponse.self,
18+
from: json(named: "user")
19+
)
1420
XCTAssertNil(response.session)
1521
}
1622
}

Tests/AuthTests/MockHelpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ func json(named name: String) -> Data {
99

1010
extension Decodable {
1111
init(fromMockNamed name: String) {
12-
self = try! JSONDecoder.goTrue.decode(Self.self, from: json(named: name))
12+
self = try! AuthClient.Configuration.jsonDecoder.decode(Self.self, from: json(named: name))
1313
}
1414
}

Tests/AuthTests/Mocks/Mocks.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ extension Session {
9797
accessToken: "accesstoken",
9898
tokenType: "bearer",
9999
expiresIn: 120,
100+
expiresAt: Date().addingTimeInterval(120).timeIntervalSince1970,
100101
refreshToken: "refreshtoken",
101102
user: User(fromMockNamed: "user")
102103
)
@@ -105,6 +106,7 @@ extension Session {
105106
accessToken: "accesstoken",
106107
tokenType: "bearer",
107108
expiresIn: 60,
109+
expiresAt: Date().addingTimeInterval(60).timeIntervalSince1970,
108110
refreshToken: "refreshtoken",
109111
user: User(fromMockNamed: "user")
110112
)

Tests/AuthTests/RequestsTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,14 @@ final class RequestsTests: XCTestCase {
166166
return (json(named: "user"), HTTPURLResponse())
167167
})
168168

169+
let currentDate = Date()
170+
169171
try await withDependencies {
170172
$0.sessionManager.update = { _ in }
171173
$0.sessionStorage.storeSession = { _ in }
172174
$0.codeVerifierStorage.getCodeVerifier = { nil }
173175
$0.eventEmitter = .live
176+
$0.currentDate = { currentDate }
174177
} operation: {
175178
let url = URL(
176179
string:
@@ -182,6 +185,7 @@ final class RequestsTests: XCTestCase {
182185
accessToken: "accesstoken",
183186
tokenType: "bearer",
184187
expiresIn: 60,
188+
expiresAt: currentDate.addingTimeInterval(60).timeIntervalSince1970,
185189
refreshToken: "refreshtoken",
186190
user: User(fromMockNamed: "user")
187191
)

0 commit comments

Comments
 (0)