diff --git a/Package.swift b/Package.swift index 66ff86a1..976f6e71 100644 --- a/Package.swift +++ b/Package.swift @@ -5,10 +5,10 @@ import PackageDescription let package = Package( name: "AppStoreConnect", platforms: [ - .iOS(.v15), - .macOS(.v12), - .tvOS(.v15), - .watchOS(.v8), + .iOS(.v16), + .macOS(.v13), + .tvOS(.v16), + .watchOS(.v9), ], products: [ .library( diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 3d32471f..f8ceca8f 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -5,10 +5,10 @@ import PackageDescription let package = Package( name: "AppStoreConnect", platforms: [ - .iOS(.v15), - .macOS(.v12), - .tvOS(.v15), - .watchOS(.v8), + .iOS(.v16), + .macOS(.v13), + .tvOS(.v16), + .watchOS(.v9), ], products: [ .library( diff --git a/README.md b/README.md index 37597f3b..0c61fb4f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ print(apps) ## Installation -This project supports Swift 5.9 and higher, and has minimum requirements of iOS 15, macOS 12, tvOS 15, and watchOS 8. It strives to be fully supported for deployment on all other platforms outlined by Swift.org [Platform Support page](https://www.swift.org/platform-support/#deployment-only), such as the various Linux flavors and Windows. App Store Connect API version 3.5 and Enterprise Program 1.0 are supported. +This project supports Swift 5.9 and higher, and has minimum requirements of iOS 16, macOS 13, tvOS 16, and watchOS 9. It strives to be fully supported for deployment on all other platforms outlined by Swift.org [Platform Support page](https://www.swift.org/platform-support/#deployment-only), such as the various Linux flavors and Windows. App Store Connect API version 3.5 and Enterprise Program 1.0 are supported. The package defines two products: `AppStoreConnect` and `EnterpriseProgram`. Each product provides the `AppStoreConnect` module, which contains the client and authentication logic, and either the `AppStoreAPI` or `EnterpriseAPI` modules, respectively. To integrate with App Store Connect, you would add a dependency on the `"AppStoreConnect"` product. To use the Enterprise Program API, add the `"EnterpriseProgram"` product as a target dependency instead. Finally, both products can be made dependencies of the same target without significant conflict. See the [invite_user](/Examples/invite_user/InviteUser.swift) sample for a rough example of this. diff --git a/Sources/AppStoreConnect/AppStoreConnectClient.swift b/Sources/AppStoreConnect/AppStoreConnectClient.swift index b7362f05..24e380ff 100644 --- a/Sources/AppStoreConnect/AppStoreConnectClient.swift +++ b/Sources/AppStoreConnect/AppStoreConnectClient.swift @@ -84,8 +84,10 @@ public actor AppStoreConnectClient { /// - currentPage: The API object representing the current page. /// - Returns: The response from the App Store Connect API. /// - Throws: An error describing the manner in which the request failed to complete. - public func send(_ request: Request, pageAfter currentPage: Response) async throws -> Response? - where Response: Decodable { + public func send( + _ request: Request, + pageAfter currentPage: Response + ) async throws -> Response? where Response: Decodable { guard let nextPage = pagedDocumentLinks(currentPage)?.next else { return nil } diff --git a/Tests/AppStoreConnectTests/AppStoreConnectClientTests.swift b/Tests/AppStoreConnectTests/AppStoreConnectClientTests.swift index 0275bc5a..5075f6ad 100644 --- a/Tests/AppStoreConnectTests/AppStoreConnectClientTests.swift +++ b/Tests/AppStoreConnectTests/AppStoreConnectClientTests.swift @@ -8,7 +8,7 @@ import XCTest import FoundationNetworking #endif -final class AppStoreConnectTests: XCTestCase { +final class AppStoreConnectClientTests: XCTestCase { private struct TestData { enum Case { case success diff --git a/Tests/AppStoreConnectTests/TransportTests.swift b/Tests/AppStoreConnectTests/TransportTests.swift index f7e7a594..37427d64 100644 --- a/Tests/AppStoreConnectTests/TransportTests.swift +++ b/Tests/AppStoreConnectTests/TransportTests.swift @@ -8,4 +8,174 @@ import XCTest import FoundationNetworking #endif -class TransportTests: XCTestCase {} +class TransportTests: XCTestCase { + private func createSession() -> URLSession { + let config = URLSessionConfiguration.ephemeral + config.protocolClasses = [MockURLProtocol.self] + + return URLSession(configuration: config) + } + + private class MockURLProtocol: URLProtocol { + typealias ResponseMaker = @Sendable (URLRequest) throws -> Response + + static let knownRequests: [URLRequest: ResponseMaker] = [ + URLRequest(url: URL(string: "https://example.com/test-send-async")!): MockData.mockingSuccessNoContent( + for:), + URLRequest(url: URL(string: "https://example.com/test-send-async-error")!): MockData.mockingError(for:), + URLRequest(url: URL(string: "https://example.com/test-send-closure")!): MockData.mockingSuccessNoContent( + for:), + URLRequest(url: URL(string: "https://example.com/test-send-closure-error")!): MockData.mockingError(for:), + URLRequest(url: URL(string: "https://example.com/test-download-async")!): MockData.mockingSuccessNoContent( + for:), + URLRequest(url: URL(string: "https://example.com/test-download-async-error")!): MockData.mockingError(for:), + URLRequest(url: URL(string: "https://example.com/test-download-closure")!): MockData + .mockingSuccessNoContent(for:), + URLRequest(url: URL(string: "https://example.com/test-download-closure-error")!): MockData.mockingError( + for:), + URLRequest(url: URL(string: "https://example.com/test-upload-async")!): MockData.mockingSuccessNoContent( + for:), + URLRequest(url: URL(string: "https://example.com/test-upload-async-error")!): MockData.mockingError(for:), + URLRequest(url: URL(string: "https://example.com/test-upload-closure")!): MockData.mockingSuccessNoContent( + for:), + URLRequest(url: URL(string: "https://example.com/test-upload-closure-error")!): MockData.mockingError(for:), + ] + + override class func canInit(with request: URLRequest) -> Bool { + knownRequests.keys.contains(request) + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + do { + let response = try MockURLProtocol.knownRequests[request]!(request) + let (data, urlResponse) = (response.data ?? Data(), response.response) + + self.client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .notAllowed) + self.client?.urlProtocol(self, didLoad: data) + self.client?.urlProtocolDidFinishLoading(self) + } catch { + self.client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} + } + + func testURLSessionSendRequest() async throws { + let request = URLRequest(url: URL(string: "https://example.com/test-send-async")!) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(decodeISO8601Date(with:)) + _ = try await createSession() + .send(request: request, decoder: decoder) + } + + func testURLSessionSendRequestFailure() async throws { + let request = URLRequest(url: URL(string: "https://example.com/test-send-async-error")!) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(decodeISO8601Date(with:)) + try await XCTAssertThrowsError( + await createSession() + .send(request: request, decoder: decoder) + ) + } + + func testURLSessionSendRequestCompletion() { + let request = URLRequest(url: URL(string: "https://example.com/test-send-closure")!) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(decodeISO8601Date(with:)) + let expectation = XCTestExpectation(description: "test-send-closure") + createSession() + .send(request: request, decoder: decoder) { result in + XCTAssertNoThrow({ try result.get() }) + expectation.fulfill() + } + } + + func testURLSessionSendRequestCompletionFailure() { + let request = URLRequest(url: URL(string: "https://example.com/test-send-closure-error")!) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(decodeISO8601Date(with:)) + let expectation = XCTestExpectation(description: "test-send-closure-error") + createSession() + .send(request: request, decoder: decoder) { result in + expectation.fulfill() + } + } + + func testURLSessionDownloadRequest() async throws { + let request = URLRequest(url: URL(string: "https://example.com/test-download-async")!) + _ = try await createSession() + .download(request: request) + } + + func testURLSessionDownloadRequestFailure() async throws { + let request = URLRequest(url: URL(string: "https://example.com/test-download-async-error")!) + try await XCTAssertThrowsError( + await createSession().download(request: request) + ) + } + + func testURLSessionDownloadRequestCompletion() { + let request = URLRequest(url: URL(string: "https://example.com/test-download-closure")!) + let expectation = XCTestExpectation(description: "test-download-closure") + createSession() + .download(request: request) { result in + XCTAssertNoThrow({ try result.get() }) + expectation.fulfill() + } + } + + func testURLSessionDownloadRequestCompletionFailure() { + let request = URLRequest(url: URL(string: "https://example.com/test-download-closure-error")!) + let expectation = XCTestExpectation(description: "test-download-closure-error") + createSession() + .download(request: request) { result in + expectation.fulfill() + } + } + + func testURLSessionUploadRequest() async throws { + let request = URLRequest(url: URL(string: "https://example.com/test-upload-async")!) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(decodeISO8601Date(with:)) + _ = try await createSession() + .upload(request: request, data: Data(), decoder: decoder) + } + + func testURLSessionUploadRequestFailure() async throws { + let request = URLRequest(url: URL(string: "https://example.com/test-upload-async-error")!) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(decodeISO8601Date(with:)) + try await XCTAssertThrowsError( + await createSession() + .upload(request: request, data: Data(), decoder: decoder) + ) + } + + func testURLSessionUploadRequestCompletion() { + let request = URLRequest(url: URL(string: "https://example.com/test-upload-closure")!) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(decodeISO8601Date(with:)) + let expectation = XCTestExpectation(description: "test-upload-closure") + createSession() + .upload(request: request, data: Data(), decoder: decoder) { result in + XCTAssertNoThrow({ try result.get() }) + expectation.fulfill() + } + } + + func testURLSessionUploadRequestCompletionFailure() { + let request = URLRequest(url: URL(string: "https://example.com/test-upload-closure-error")!) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(decodeISO8601Date(with:)) + let expectation = XCTestExpectation(description: "test-upload-closure-error") + createSession() + .upload(request: request, data: Data(), decoder: decoder) { result in + expectation.fulfill() + } + } +} diff --git a/Tests/Mocks/MockData.swift b/Tests/Mocks/MockData.swift index c812bbf3..665aa5e0 100644 --- a/Tests/Mocks/MockData.swift +++ b/Tests/Mocks/MockData.swift @@ -78,9 +78,10 @@ public enum MockData { } extension MockData { - public static func mockingSuccess(with content: Content, url: URL = URL()) throws -> Response< - Data - > { + public static func mockingSuccess( + with content: Content, + url: URL = URL() + ) throws -> Response { let data = try MockData.encoder.encode(content) return .init(data: data, response: urlResponse(for: url, statusCode: 200), statusCode: 200, rate: nil) }