diff --git a/Sources/AppStoreConnectClient/AppStoreConnectClient.swift b/Sources/AppStoreConnectClient/AppStoreConnectClient.swift index b831c3c..7702f0a 100644 --- a/Sources/AppStoreConnectClient/AppStoreConnectClient.swift +++ b/Sources/AppStoreConnectClient/AppStoreConnectClient.swift @@ -26,7 +26,7 @@ import OpenAPIURLSession import Foundation /// The client for interacting with the App Store Connect API. -public struct AppStoreConnectClient { +public struct AppStoreConnectClient: AppStoreConnectClientProtocol { private let client: any APIProtocol /// Initializes the client with a custom API protocol implementation. @@ -124,20 +124,26 @@ public struct AppStoreConnectClient { /// - Throws: An error of type `AppStoreConnectError` if the response indicates an error. public func fetchBuilds( for app: Application, - with query: BuildsQuery + with query: BuildsQuery? = nil ) async throws -> [Build] { - let response = try await client.builds_hyphen_get_collection( - query: .init( - filter_lbrack_app_rbrack_: [app.id], - sort: type(of: query.sort).init(arrayLiteral: ._hyphen_version), - fields_lbrack_builds_rbrack_: type(of: query.fields).init( - arrayLiteral: - .version, - .minOsVersion, - .uploadedDate + var response: Operations.builds_hyphen_get_collection.Output + if let query = query { + let sort = query.convert(from: query.sort) + let fields = query.convert(from: query.fields) + response = try await client.builds_hyphen_get_collection( + query: .init( + filter_lbrack_app_rbrack_: [app.id], + sort: sort, + fields_lbrack_builds_rbrack_: fields ) ) - ) + } else { + response = try await client.builds_hyphen_get_collection( + query: .init( + filter_lbrack_app_rbrack_: [app.id] + ) + ) + } switch response { case .ok(let okResponse): return try okResponse.body.json.data.compactMap { Build(schema: $0) } @@ -164,7 +170,7 @@ public struct AppStoreConnectClient { /// - Parameter id: The build-id for which to fetch the pre-release version. /// - Returns: A `PreReleaseVersion` object. /// - Throws: An error of type `AppStoreConnectError` if the response indicates an error. - public func fetchPreReleaseVersion(for build: Build) async throws -> PreReleaseVersion { + public func fetchPreReleaseVersion(by build: Build) async throws -> PreReleaseVersion { let id = build.id let response = try await client.builds_hyphen_preReleaseVersion_hyphen_get_to_one_related( path: .init(id: id) diff --git a/Sources/AppStoreConnectClient/AppStoreConnectClientProtocol.swift b/Sources/AppStoreConnectClient/AppStoreConnectClientProtocol.swift index db153e8..20009ac 100644 --- a/Sources/AppStoreConnectClient/AppStoreConnectClientProtocol.swift +++ b/Sources/AppStoreConnectClient/AppStoreConnectClientProtocol.swift @@ -48,7 +48,7 @@ public protocol AppStoreConnectClientProtocol { /// - Note: Builds are specific versions of an app that have been uploaded to App Store Connect. func fetchBuilds( for app: Application, - with query: BuildsQuery + with query: BuildsQuery? ) async throws -> [Build] /// Fetches the pre-release testFlight's version associated with a specific build from App Store Connect. diff --git a/Sources/AppStoreConnectClient/Models/Build.swift b/Sources/AppStoreConnectClient/Models/Build.swift index a05f5fc..06925ba 100644 --- a/Sources/AppStoreConnectClient/Models/Build.swift +++ b/Sources/AppStoreConnectClient/Models/Build.swift @@ -32,7 +32,7 @@ import Foundation /// - minOsVersion: The minimum required OS version for the build. /// - Note: A build is a specific version of an app that has been uploaded to App Store Connect. /// It contains metadata such as the build version, upload date, and minimum supported OS version. -public struct Build { +public struct Build: Equatable { /// The unique identifier of the build. public let id: String /// The version string of the build. diff --git a/Sources/AppStoreConnectClient/Models/BuildsQuery.swift b/Sources/AppStoreConnectClient/Models/BuildsQuery.swift index e012aa2..3b80778 100644 --- a/Sources/AppStoreConnectClient/Models/BuildsQuery.swift +++ b/Sources/AppStoreConnectClient/Models/BuildsQuery.swift @@ -27,7 +27,63 @@ import Foundation /// A structure representing a query for builds with specified sort and fields parameters. public struct BuildsQuery { /// The sort payload for the query. - let sort: Operations.builds_hyphen_get_collection.Input.Query.sortPayload + public var sort: [Sort] = [] /// The fields payload for the query. - let fields: Operations.builds_hyphen_get_collection.Input.Query.fields_lbrack_builds_rbrack_Payload + public var fields: [Fields] = [] + + /// Converts the Sort enum array to the appropriate payload type used in the query. + /// - Parameter from: The Sort enum value to convert. + /// - Returns: The converted sort payload. + func convert(from sorts: [Sort]) -> [Operations.builds_hyphen_get_collection.Input.Query.sortPayloadPayload] { + return sorts.compactMap { sort in + return Operations.builds_hyphen_get_collection.Input.Query.sortPayloadPayload(rawValue: sort.rawValue) + } + } + + /// Converts the Fields enum array to the appropriate payload type used in the query. + /// - Parameter from: The array of Fields enum values to convert. + /// - Returns: An array of converted field payloads. + func convert(from fields: [Fields]) -> [Operations.builds_hyphen_get_collection.Input.Query.fields_lbrack_builds_rbrack_PayloadPayload] { + return fields.compactMap { field in + return Operations.builds_hyphen_get_collection.Input.Query.fields_lbrack_builds_rbrack_PayloadPayload(rawValue: field.rawValue) + } + } + + /// An enumeration representing the possible sort options for the query. + public enum Sort: String { + case preReleaseVersion + case hyphenPreReleaseVersion = "-preReleaseVersion" + case uploadedDate + case hyphenUploadedDate = "-uploadedDate" + case version + case hyphenVersion = "-version" + } + + /// An enumeration representing the possible fields that can be included in the query. + public enum Fields: String { + case app + case appEncryptionDeclaration + case appStoreVersion + case betaAppReviewSubmission + case betaBuildLocalizations + case betaGroups + case buildAudienceType + case buildBetaDetail + case buildBundles + case computedMinMacOsVersion + case diagnosticSignatures + case expirationDate + case expired + case iconAssetToken + case icons + case individualTesters + case lsMinimumSystemVersion + case minOsVersion + case perfPowerMetrics + case preReleaseVersion + case processingState + case uploadedDate + case usesNonExemptEncryption + case version + } } diff --git a/Tests/AppStoreConnectClientTests/AppStoreConnectClientTests.swift b/Tests/AppStoreConnectClientTests/AppStoreConnectClientTests.swift index 7124336..29ecc9c 100644 --- a/Tests/AppStoreConnectClientTests/AppStoreConnectClientTests.swift +++ b/Tests/AppStoreConnectClientTests/AppStoreConnectClientTests.swift @@ -199,15 +199,12 @@ final class AppStoreConnectClientTests: XCTestCase { let expectedBuilds = [Build(schema: MockObjects.build)] let build = try await client.fetchBuilds( for: app, - with: BuildsQuery.init( - sort: .init(arrayLiteral: ._hyphen_version), - fields: .init(arrayLiteral: .version, .minOsVersion, .uploadedDate) + with: BuildsQuery( + sort: [.hyphenVersion], + fields: [.version, .minOsVersion, .uploadedDate] ) ) - XCTAssertEqual(build.first?.id, expectedBuilds.first?.id) - XCTAssertEqual(build.first?.version, expectedBuilds.first?.version) - XCTAssertEqual(build.first?.uploadedDate, expectedBuilds.first?.uploadedDate) - XCTAssertEqual(build.first?.minOsVersion, expectedBuilds.first?.minOsVersion) + XCTAssertEqual(build, expectedBuilds) } func testFetchBuildsBadRequest() async throws { @@ -219,12 +216,12 @@ final class AppStoreConnectClientTests: XCTestCase { do { let result = try await client.fetchBuilds( for: app, - with: BuildsQuery.init( - sort: .init(arrayLiteral: ._hyphen_version), - fields: .init(arrayLiteral: .version, .minOsVersion, .uploadedDate) + with: BuildsQuery( + sort: [.hyphenVersion, .uploadedDate], + fields: [.version, .minOsVersion, .uploadedDate] ) ) - XCTFail("Expected error not thrown, got result: \(result)") + XCTFail("Expected error not thrown, got result: \(String(describing: result))") } catch AppStoreConnectError.badRequest(let error) { XCTAssertEqual(error, expectedError) } catch { @@ -241,12 +238,12 @@ final class AppStoreConnectClientTests: XCTestCase { do { let result = try await client.fetchBuilds( for: app, - with: BuildsQuery.init( - sort: .init(arrayLiteral: ._hyphen_version), - fields: .init(arrayLiteral: .version, .minOsVersion, .uploadedDate) + with: BuildsQuery( + sort: [.hyphenVersion, .uploadedDate], + fields: [.version, .minOsVersion, .uploadedDate] ) ) - XCTFail("Expected error not thrown, got result: \(result)") + XCTFail("Expected error not thrown, got result: \(String(describing: result))") } catch AppStoreConnectError.unauthorized(let error) { XCTAssertEqual(error, expectedError) } catch { @@ -263,12 +260,12 @@ final class AppStoreConnectClientTests: XCTestCase { do { let result = try await client.fetchBuilds( for: app, - with: BuildsQuery.init( - sort: .init(arrayLiteral: ._hyphen_version), - fields: .init(arrayLiteral: .version, .minOsVersion, .uploadedDate) + with: BuildsQuery( + sort: [.hyphenVersion, .uploadedDate], + fields: [.version, .minOsVersion, .uploadedDate] ) ) - XCTFail("Expected error not thrown, got result: \(result)") + XCTFail("Expected error not thrown, got result: \(String(describing: result))") } catch AppStoreConnectError.forbidden(let error) { XCTAssertEqual(error, expectedError) } catch { @@ -285,12 +282,12 @@ final class AppStoreConnectClientTests: XCTestCase { do { let result = try await client.fetchBuilds( for: app, - with: BuildsQuery.init( - sort: .init(arrayLiteral: ._hyphen_version), - fields: .init(arrayLiteral: .version, .minOsVersion, .uploadedDate) + with: BuildsQuery( + sort: [.hyphenVersion, .uploadedDate], + fields: [.version, .minOsVersion, .uploadedDate] ) ) - XCTFail("Expected error not thrown, got result: \(result)") + XCTFail("Expected error not thrown, got result: \(String(describing: result))") } catch AppStoreConnectError.serverError(let error) { XCTAssertEqual(error, expectedError) } catch { @@ -303,7 +300,7 @@ final class AppStoreConnectClientTests: XCTestCase { mockClient.result = .ok let client = AppStoreConnectClient(client: mockClient) let build = Build(schema: MockObjects.build) - let preReleaseVersion = try await client.fetchPreReleaseVersion(for: build) + let preReleaseVersion = try await client.fetchPreReleaseVersion(by: build) XCTAssertEqual(preReleaseVersion.id, "FooBarId") XCTAssertEqual(preReleaseVersion.version, "Foo") XCTAssertEqual(preReleaseVersion.platform, "IOS") @@ -317,7 +314,7 @@ final class AppStoreConnectClientTests: XCTestCase { let build = Build(schema: MockObjects.build) let expectedError = "\nFailed with: \(badRequest), Foo, Bar, Baz." do { - let result = try await client.fetchPreReleaseVersion(for: build) + let result = try await client.fetchPreReleaseVersion(by: build) XCTFail("Expected error not thrown, got result: \(result)") } catch AppStoreConnectError.badRequest(let error) { XCTAssertEqual(error, expectedError) @@ -333,7 +330,7 @@ final class AppStoreConnectClientTests: XCTestCase { let build = Build(schema: MockObjects.build) let expectedError = "\nFailed with: \(unauthorized), Foo, Bar, Baz." do { - let result = try await client.fetchPreReleaseVersion(for: build) + let result = try await client.fetchPreReleaseVersion(by: build) XCTFail("Expected error not thrown, got result: \(result)") } catch AppStoreConnectError.unauthorized(let error) { XCTAssertEqual(error, expectedError) @@ -349,7 +346,7 @@ final class AppStoreConnectClientTests: XCTestCase { let build = Build(schema: MockObjects.build) let expectedError = "\nFailed with: \(notFound), Foo, Bar, Baz." do { - let result = try await client.fetchPreReleaseVersion(for: build) + let result = try await client.fetchPreReleaseVersion(by: build) XCTFail("Expected error not thrown, got result: \(result)") } catch AppStoreConnectError.notFound(let error) { XCTAssertEqual(error, expectedError) @@ -365,7 +362,7 @@ final class AppStoreConnectClientTests: XCTestCase { let build = Build(schema: MockObjects.build) let expectedError = "\nFailed with: \(forbidden), Foo, Bar, Baz." do { - let result = try await client.fetchPreReleaseVersion(for: build) + let result = try await client.fetchPreReleaseVersion(by: build) XCTFail("Expected error not thrown, got result: \(result)") } catch AppStoreConnectError.forbidden(let error) { XCTAssertEqual(error, expectedError) @@ -381,7 +378,7 @@ final class AppStoreConnectClientTests: XCTestCase { let build = Build(schema: MockObjects.build) let expectedError = 501 do { - let result = try await client.fetchPreReleaseVersion(for: build) + let result = try await client.fetchPreReleaseVersion(by: build) XCTFail("Expected error not thrown, got result: \(result)") } catch AppStoreConnectError.serverError(let error) { XCTAssertEqual(error, expectedError) @@ -389,4 +386,52 @@ final class AppStoreConnectClientTests: XCTestCase { XCTFail("Unexpected error: \(error)") } } + + func testFetchBuildsSuccessWithEmptySort() async throws { + var mockClient = MockAPIClient() + mockClient.result = .ok + let client = AppStoreConnectClient(client: mockClient) + let app = Application(id: "Foo", bundleId: "Bar") + let expectedBuilds = [Build(schema: MockObjects.build)] + let build = try await client.fetchBuilds( + for: app, + with: BuildsQuery( + sort: [], + fields: [.version, .minOsVersion, .uploadedDate] + ) + ) + XCTAssertEqual(build, expectedBuilds) + } + + func testFetchBuildsSuccessWithEmptyFields() async throws { + var mockClient = MockAPIClient() + mockClient.result = .ok + let client = AppStoreConnectClient(client: mockClient) + let app = Application(id: "Foo", bundleId: "Bar") + let expectedBuilds = [Build(schema: MockObjects.build)] + let build = try await client.fetchBuilds( + for: app, + with: BuildsQuery( + sort: [.hyphenVersion, .uploadedDate], + fields: [] + ) + ) + XCTAssertEqual(build, expectedBuilds) + } + + func testFetchBuildsSuccessWithoutQuery() async throws { + var mockClient = MockAPIClient() + mockClient.result = .ok + let client = AppStoreConnectClient(client: mockClient) + let app = Application(id: "Foo", bundleId: "Bar") + let expectedBuilds = [Build(schema: MockObjects.build)] + let build = try await client.fetchBuilds(for: app) + XCTAssertEqual(build, expectedBuilds) + } + + func testBuildsQueryInitialization() { + let query = BuildsQuery() + XCTAssertEqual(query.sort, []) + XCTAssertEqual(query.fields, []) + } }