Overhauled the library with several new features #5
No files matched your search
@@ -54,6 +54,14 @@ let client = Client(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Tokens expire after 20 minutes at most. For longer-lived clients, pass a token provider instead, which is called on every request:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
BearerAuthMiddleware {
|
||||||
|
try await tokenStore.validToken()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Making API Calls
|
### Making API Calls
|
||||||
|
|
||||||
The `Client` conforms to `APIProtocol`, which defines a method for every endpoint in the App Store Connect API. Each method accepts an `Input` value and returns an `Output` value with the response.
|
The `Client` conforms to `APIProtocol`, which defines a method for every endpoint in the App Store Connect API. Each method accepts an `Input` value and returns an `Output` value with the response.
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ let client = Client(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Tokens expire after 20 minutes at most. For longer-lived clients, pass a token provider instead, which is called on every request:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
BearerAuthMiddleware {
|
||||||
|
try await tokenStore.validToken()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Making API Calls
|
### Making API Calls
|
||||||
|
|
||||||
The ``Client`` conforms to ``APIProtocol``, which defines a method for every endpoint in the App Store Connect API. Each method accepts an `Input` value and returns an `Output` value with the response.
|
The ``Client`` conforms to ``APIProtocol``, which defines a method for every endpoint in the App Store Connect API. Each method accepts an `Input` value and returns an `Output` value with the response.
|
||||||
|
|||||||
@@ -13,10 +13,6 @@
|
|||||||
// ===----------------------------------------------------------------------===
|
// ===----------------------------------------------------------------------===
|
||||||
|
|
||||||
import struct Foundation.URL
|
import struct Foundation.URL
|
||||||
import struct Foundation.URLComponents
|
|
||||||
import struct Foundation.URLQueryItem
|
|
||||||
import struct HTTPTypes.HTTPField
|
|
||||||
import struct HTTPTypes.HTTPFields
|
|
||||||
import struct HTTPTypes.HTTPRequest
|
import struct HTTPTypes.HTTPRequest
|
||||||
import struct HTTPTypes.HTTPResponse
|
import struct HTTPTypes.HTTPResponse
|
||||||
import protocol OpenAPIRuntime.ClientMiddleware
|
import protocol OpenAPIRuntime.ClientMiddleware
|
||||||
@@ -30,21 +26,43 @@ import class OpenAPIRuntime.HTTPBody
|
|||||||
///
|
///
|
||||||
/// ## Usage
|
/// ## Usage
|
||||||
///
|
///
|
||||||
|
/// Provide a fixed token:
|
||||||
|
///
|
||||||
/// ```swift
|
/// ```swift
|
||||||
/// let middleware = BearerAuthMiddleware(token: "your-jwt-token")
|
/// let middleware = BearerAuthMiddleware(token: "your-jwt-token")
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// Tokens expire after 20 minutes at most, so long-lived clients should provide a token provider instead:
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// let middleware = BearerAuthMiddleware {
|
||||||
|
/// try await tokenStore.validToken()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
public struct BearerAuthMiddleware {
|
public struct BearerAuthMiddleware {
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
/// The Bearer token to include in the `Authorization` header of each request.
|
/// The closure that provides the Bearer token for each request.
|
||||||
private let token: String
|
private let tokenProvider: @Sendable () async throws -> String
|
||||||
|
|
||||||
// MARK: Initializers
|
// MARK: Initializers
|
||||||
|
|
||||||
/// Creates a new middleware instance with the given Bearer token.
|
/// Creates a new middleware instance with a fixed Bearer token.
|
||||||
/// - Parameter token: A JSON Web Token (JWT) string used to authenticate requests to the App Store Connect API.
|
/// - Parameter token: A JSON Web Token (JWT) used to authenticate requests to the App Store Connect API.
|
||||||
init(token: String) {
|
public init(
|
||||||
self.token = token
|
token: String
|
||||||
|
) {
|
||||||
|
self.init { token }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new middleware instance that asks for a Bearer token on every request.
|
||||||
|
///
|
||||||
|
/// If the provider throws, the error is rethrown and the request is not sent.
|
||||||
|
/// - Parameter tokenProvider: A closure that returns a valid JSON Web Token (JWT).
|
||||||
|
public init(
|
||||||
|
tokenProvider: @escaping @Sendable () async throws -> String
|
||||||
|
) {
|
||||||
|
self.tokenProvider = tokenProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +86,8 @@ extension BearerAuthMiddleware: ClientMiddleware {
|
|||||||
operationID: String,
|
operationID: String,
|
||||||
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
|
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
|
||||||
) async throws -> (HTTPResponse, HTTPBody?) {
|
) async throws -> (HTTPResponse, HTTPBody?) {
|
||||||
|
let token = try await tokenProvider()
|
||||||
|
|
||||||
var request = request
|
var request = request
|
||||||
request.headerFields[.authorization] = "Bearer \(token)"
|
request.headerFields[.authorization] = "Bearer \(token)"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import HTTPTypes
|
|||||||
import OpenAPIRuntime
|
import OpenAPIRuntime
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@testable import ASConnectService
|
// A non-testable import ensures that the middleware can be created from outside the module.
|
||||||
|
import ASConnectService
|
||||||
|
|
||||||
@Suite("BearerAuthMiddleware")
|
@Suite("BearerAuthMiddleware")
|
||||||
struct BearerAuthMiddlewareTests {
|
struct BearerAuthMiddlewareTests {
|
||||||
@@ -112,6 +113,67 @@ struct BearerAuthMiddlewareTests {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Asks the token provider for a token on every request")
|
||||||
|
func asksTokenProviderOnEveryRequest() async throws {
|
||||||
|
let tokens = TokenSequence()
|
||||||
|
let middleware = BearerAuthMiddleware {
|
||||||
|
await tokens.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
for expectedToken in ["token-1", "token-2"] {
|
||||||
|
_ = try await middleware.intercept(
|
||||||
|
.get,
|
||||||
|
body: nil,
|
||||||
|
baseURL: .base,
|
||||||
|
operationID: "listApps",
|
||||||
|
next: { interceptedRequest, _, _ in
|
||||||
|
#expect(interceptedRequest.headerFields[.authorization] == "Bearer \(expectedToken)")
|
||||||
|
|
||||||
|
return (HTTPResponse(status: .ok), nil)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Rethrows the token provider error without calling the next handler")
|
||||||
|
func rethrowsTokenProviderError() async throws {
|
||||||
|
let middleware = BearerAuthMiddleware {
|
||||||
|
throw TokenError.expired
|
||||||
|
}
|
||||||
|
|
||||||
|
await #expect(throws: TokenError.expired) {
|
||||||
|
_ = try await middleware.intercept(
|
||||||
|
.get,
|
||||||
|
body: nil,
|
||||||
|
baseURL: .base,
|
||||||
|
operationID: "listApps",
|
||||||
|
next: { _, _, _ in
|
||||||
|
Issue.record("The next handler should not be called when the token provider throws")
|
||||||
|
|
||||||
|
return (HTTPResponse(status: .ok), nil)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
/// An actor that returns a new token every time it is asked for one.
|
||||||
|
private actor TokenSequence {
|
||||||
|
private var count = 0
|
||||||
|
|
||||||
|
func next() -> String {
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return "token-\(count)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error thrown by a token provider.
|
||||||
|
private enum TokenError: Error {
|
||||||
|
case expired
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - HTTPRequest+Samples
|
// MARK: - HTTPRequest+Samples
|
||||||
|
|||||||
Reference in new issue
Block a user