Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make BagbutikService an actor and the Request types Sendable #203

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Bagbutik-Core/Parameters/EndpointParameter.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Parameter for an endpoint
public protocol EndpointParameter {
public protocol EndpointParameter: Sendable {
/// The name of the case to use as value for the parameter.
var caseName: String { get }
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Bagbutik-Core/Parameters/Parameters.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// The parameters for a `Request`.
public struct Parameters {
public struct Parameters: Sendable {
/// Fields to return for included related types.
public let fields: [FieldParameter]?
/// Attributes, relationships, and IDs by which to filter
Expand Down
6 changes: 5 additions & 1 deletion Sources/Bagbutik-Core/Service/BagbutikService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public typealias FetchData = (_ request: URLRequest, _ delegate: URLSessionTaskD

If the JWT has expired, it will be renewed before the request is performed.
*/
public class BagbutikService {
public actor BagbutikService {
internal var jwt: JWT
private let fetchData: FetchData

Expand Down Expand Up @@ -165,4 +165,8 @@ public class BagbutikService {
}
throw ServiceError.unknown(data: data)
}

internal func replaceJWT(_ jwt: JWT) {
self.jwt = jwt
}
}
2 changes: 1 addition & 1 deletion Sources/Bagbutik-Core/Service/HTTPMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
Documentation borrowed from:
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods>
*/
public enum HTTPMethod: String {
public enum HTTPMethod: String, Sendable {
/// The GET method requests a representation of the specified resource.
case get = "GET"
/// The PUT method replaces all current representations of the target resource with the request payload.
Expand Down
28 changes: 20 additions & 8 deletions Sources/Bagbutik-Core/Service/JWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
Full documentation for how JWT is used with the API:
<https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests>
*/
public struct JWT {
public struct JWT: Sendable {
/// A value telling if the JWT has expired.
public var isExpired: Bool { payload.isExpired }
/// The signature to use in the authorization header when performing requests.
Expand All @@ -22,7 +22,7 @@ public struct JWT {
private var payload: Payload
private let privateKey: String

internal var dateFactory: (TimeInterval) -> Date {
internal var dateFactory: DateFactory {
didSet { payload.dateFactory = dateFactory }
}

Expand All @@ -38,7 +38,7 @@ public struct JWT {
- privateKey: The contents of your private key from App Store Connect. Starting with `-----BEGIN PRIVATE KEY-----`.
*/
public init(keyId: String, issuerId: String, privateKey: String) throws {
try self.init(keyId: keyId, issuerId: issuerId, privateKey: privateKey, dateFactory: Date.init(timeIntervalSinceNow:))
try self.init(keyId: keyId, issuerId: issuerId, privateKey: privateKey, dateFactory: DateFactory())
}

/**
Expand All @@ -57,7 +57,7 @@ public struct JWT {
try self.init(keyId: keyId, issuerId: issuerId, privateKey: privateKey)
}

init(keyId: String, issuerId: String, privateKey: String, dateFactory: @escaping (TimeInterval) -> Date) throws {
init(keyId: String, issuerId: String, privateKey: String, dateFactory: DateFactory) throws {
header = Header(kid: keyId)
payload = Payload(iss: issuerId, dateFactory: dateFactory)
self.privateKey = privateKey
Expand Down Expand Up @@ -87,14 +87,14 @@ public struct JWT {
let typ = "kid"
}

private struct Payload: Encodable {
private struct Payload: Encodable, Sendable {
let iss: String
private(set) var exp: Int
let aud = "appstoreconnect-v1"
var dateFactory: (TimeInterval) -> Date
var dateFactory: DateFactory
var isExpired: Bool { Date(timeIntervalSince1970: TimeInterval(exp)) < Date(timeIntervalSinceNow: 0) }

init(iss: String, dateFactory: @escaping (TimeInterval) -> Date) {
init(iss: String, dateFactory: DateFactory) {
self.iss = iss
self.dateFactory = dateFactory
exp = 0
Expand All @@ -113,7 +113,19 @@ public struct JWT {
}

mutating func renewExp() {
exp = Int(dateFactory(20 * 60).timeIntervalSince1970)
exp = Int(dateFactory.createDate(fromTimeIntervalSinceNow: 20 * 60).timeIntervalSince1970)
}
}
}

internal struct DateFactory: Sendable {
let now: Date?

init(now: Date? = nil) {
self.now = now
}

func createDate(fromTimeIntervalSinceNow timeIntervalSinceNow: TimeInterval) -> Date {
(now ?? .now).addingTimeInterval(timeIntervalSinceNow)
}
}
2 changes: 1 addition & 1 deletion Sources/Bagbutik-Core/Service/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import FoundationNetworking
private let baseUrl = URL(string: "https://api.appstoreconnect.apple.com")!

/// A description of a request. This will internally be mapped to a real URL request.
public struct Request<ResponseType, ErrorResponseType> {
public struct Request<ResponseType, ErrorResponseType>: Sendable {
/// The path of the endpoint.
public let path: String
/// The HTTP method to use for the request.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Bagbutik-Core/Service/RequestBody.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// A protocol used for structs to be sent with `Request`s.
public protocol RequestBody: Encodable {
public protocol RequestBody: Encodable, Sendable {
/// A JSON representation of the struct.
var jsonData: Data { get }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// A response with no properties.
public struct EmptyResponse: Codable {
public struct EmptyResponse: Codable, Sendable {
/// Creates a new empty response.
public init() {}
}
14 changes: 9 additions & 5 deletions Tests/Bagbutik-CoreTests/BagbutikServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ final class BagbutikServiceTests: XCTestCase {

func setUpService(expiredJWT: Bool = false) throws {
mockURLSession = .init()
let dateFactory = expiredJWT ? { _ in Date.distantPast } : Date.init(timeIntervalSinceNow:)
let dateFactory = DateFactory(now: expiredJWT ? Date.distantPast : Date.now)
jwt = try JWT(keyId: JWTTests.keyId, issuerId: JWTTests.issuerId, privateKey: JWTTests.privateKey, dateFactory: dateFactory)
let fetchData = mockURLSession.data(for:delegate:)
nonisolated(unsafe) let fetchData = mockURLSession.data(for:delegate:)
service = .init(jwt: jwt, fetchData: fetchData)
}

Expand Down Expand Up @@ -149,16 +149,20 @@ final class BagbutikServiceTests: XCTestCase {
}
}

@MainActor
func testJWTRenewal() async throws {
try setUpService(expiredJWT: true)
XCTAssertTrue(service.jwt.isExpired)
service.jwt.dateFactory = Date.init(timeIntervalSinceNow:)
let isExpiredBefore = await service.jwt.isExpired
XCTAssertTrue(isExpiredBefore)
jwt.dateFactory = .init()
await service.replaceJWT(jwt)
let request: Request<AppResponse, ErrorResponse> = .getAppV1(id: "app-id")
let expectedResponse = AppResponse(data: .init(id: "app-id", links: .init(self: "")), links: .init(self: ""))
mockURLSession.responsesByUrl[request.asUrlRequest().url!] = try (data: jsonEncoder.encode(expectedResponse),
type: .http(statusCode: 200))
_ = try await service.request(request)
XCTAssertFalse(service.jwt.isExpired)
let isExpiredAfter = await service.jwt.isExpired
XCTAssertFalse(isExpiredAfter)
}
}

Expand Down
6 changes: 3 additions & 3 deletions Tests/Bagbutik-CoreTests/JWTTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ final class JWTTests: XCTestCase {
"""

func testInitialEncodedSignature() throws {
let jwt = try JWT(keyId: Self.keyId, issuerId: Self.issuerId, privateKey: Self.privateKey, dateFactory: { _ in Date.distantFuture })
let jwt = try JWT(keyId: Self.keyId, issuerId: Self.issuerId, privateKey: Self.privateKey, dateFactory: .init(now: Date.distantFuture))
XCTAssertFalse(jwt.isExpired)
XCTAssertTrue(jwt.encodedSignature.hasPrefix("eyJ"))
}

func testInitialEncodedSignature_Renew() throws {
var jwt = try JWT(keyId: Self.keyId, issuerId: Self.issuerId, privateKey: Self.privateKey, dateFactory: { _ in Date.distantPast })
var jwt = try JWT(keyId: Self.keyId, issuerId: Self.issuerId, privateKey: Self.privateKey, dateFactory: .init(now: Date.distantPast))
XCTAssertTrue(jwt.isExpired)
jwt.dateFactory = Date.init(timeIntervalSinceNow:)
jwt.dateFactory = .init()
try jwt.renewEncodedSignature()
XCTAssertFalse(jwt.isExpired)
}
Expand Down