From 9481c262904fff434e9c144e36dd6bb82326f871 Mon Sep 17 00:00:00 2001 From: Aaron Sky Date: Fri, 8 May 2020 10:06:46 -0400 Subject: [PATCH] Added support for Buildkite's JSON errors, and wrote more tests for pagination --- LICENSE | 2 +- Sources/Buildkite/Buildkite.swift | 47 ++---- Sources/Buildkite/Models/Job.swift | 2 +- Sources/Buildkite/Networking/Response.swift | 17 +++ Sources/Buildkite/Networking/StatusCode.swift | 8 +- Sources/Buildkite/Resources/Jobs.swift | 63 ++++++-- Sources/Buildkite/Resources/Teams.swift | 2 +- Tests/BuildkiteTests/BuildkiteTests.swift | 140 +++++++++++++++--- .../Networking/PaginationTests.swift | 35 ----- .../BuildkiteTests/Resources/JobsTests.swift | 51 ++++++- Tests/BuildkiteTests/Utilities/MockData.swift | 60 +++++++- 11 files changed, 312 insertions(+), 115 deletions(-) delete mode 100644 Tests/BuildkiteTests/Networking/PaginationTests.swift diff --git a/LICENSE b/LICENSE index a87c001..4efcfcc 100644 --- a/LICENSE +++ b/LICENSE @@ -6,4 +6,4 @@ Redistribution and use in source and binary forms, with or without modification, 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Sources/Buildkite/Buildkite.swift b/Sources/Buildkite/Buildkite.swift index b95f996..ba63384 100644 --- a/Sources/Buildkite/Buildkite.swift +++ b/Sources/Buildkite/Buildkite.swift @@ -49,7 +49,7 @@ public final class Buildkite { transport.send(request: request, completion: handleContentfulResponse(completion: completion)) } - public func send(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result, Error>) -> Void) { + public func send(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result, Error>) -> Void) { let request = URLRequest(resource, configuration: configuration, pageOptions: pageOptions) transport.send(request: request, completion: handleContentfulResponse(completion: completion)) } @@ -65,7 +65,7 @@ public final class Buildkite { transport.send(request: request, completion: handleContentfulResponse(completion: completion)) } - public func send(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result, Error>) -> Void) { + public func send(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result, Error>) -> Void) { let request: URLRequest do { request = try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) @@ -92,17 +92,6 @@ public final class Buildkite { transport.send(request: request, completion: handleEmptyResponse(completion: completion)) } - public func send(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result, Error>) -> Void) { - let request: URLRequest - do { - request = try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) - } catch { - completion(.failure(error)) - return - } - transport.send(request: request, completion: handleEmptyResponse(completion: completion)) - } - private func handleContentfulResponse(completion: @escaping (Result, Error>) -> Void) -> Transport.Completion { return { [weak self] result in guard let self = self else { @@ -113,7 +102,7 @@ public final class Buildkite { do { let data: Data (data, response) = try result.get() - try self.checkResponseForIssues(response) + try self.checkResponseForIssues(response, data: data) content = try self.decoder.decode(Content.self, from: data) } catch { completion(.failure(error)) @@ -140,13 +129,18 @@ public final class Buildkite { } } - private func checkResponseForIssues(_ response: URLResponse) throws { + private func checkResponseForIssues(_ response: URLResponse, data: Data? = nil) throws { guard let response = response as? HTTPURLResponse, let statusCode = StatusCode(rawValue: response.statusCode) else { throw ResponseError.missingResponse } if !statusCode.isSuccess { - throw statusCode + if let data = data, + let errorIntermediary = try? decoder.decode(BuildkiteError.Intermediary.self, from: data) { + throw BuildkiteError(statusCode: statusCode, intermediary: errorIntermediary) + } else { + throw statusCode + } } } } @@ -159,7 +153,7 @@ extension Buildkite { public func sendPublisher(_ resource: R) -> AnyPublisher, Error> { transport.sendPublisher(request: URLRequest(resource, configuration: configuration)) .tryMap { - try self.checkResponseForIssues($0.response) + try self.checkResponseForIssues($0.response, data: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data) return Response(content: content, response: $0.response) } @@ -169,7 +163,7 @@ extension Buildkite { public func sendPublisher(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher, Error> { transport.sendPublisher(request: URLRequest(resource, configuration: configuration, pageOptions: pageOptions)) .tryMap { - try self.checkResponseForIssues($0.response) + try self.checkResponseForIssues($0.response, data: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data) return Response(content: content, response: $0.response) } @@ -181,19 +175,19 @@ extension Buildkite { .publisher .flatMap(transport.sendPublisher) .tryMap { - try self.checkResponseForIssues($0.response) + try self.checkResponseForIssues($0.response, data: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data) return Response(content: content, response: $0.response) } .eraseToAnyPublisher() } - public func sendPublisher(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher, Error> { + public func sendPublisher(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher, Error> { Result { try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) } .publisher .flatMap(transport.sendPublisher) .tryMap { - try self.checkResponseForIssues($0.response) + try self.checkResponseForIssues($0.response, data: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data) return Response(content: content, response: $0.response) } @@ -219,16 +213,5 @@ extension Buildkite { } .eraseToAnyPublisher() } - - func sendPublisher(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher, Error> { - Result { try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) } - .publisher - .flatMap(transport.sendPublisher) - .tryMap { - try self.checkResponseForIssues($0.response) - return Response(content: (), response: $0.response) - } - .eraseToAnyPublisher() - } } #endif diff --git a/Sources/Buildkite/Models/Job.swift b/Sources/Buildkite/Models/Job.swift index fa2500b..da1bf74 100644 --- a/Sources/Buildkite/Models/Job.swift +++ b/Sources/Buildkite/Models/Job.swift @@ -127,7 +127,7 @@ public enum Job: Codable, Equatable { } } - public struct LogOutput: Codable { + public struct LogOutput: Codable, Equatable { public var url: URL // Resource public var content: String public var size: Int diff --git a/Sources/Buildkite/Networking/Response.swift b/Sources/Buildkite/Networking/Response.swift index 79bbb17..0d6a312 100644 --- a/Sources/Buildkite/Networking/Response.swift +++ b/Sources/Buildkite/Networking/Response.swift @@ -17,6 +17,23 @@ enum ResponseError: Error { case unexpectedlyNoContent } +public struct BuildkiteError: Error { + public var statusCode: StatusCode + public var message: String + public var errors: [String] + + init(statusCode: StatusCode, intermediary: Intermediary) { + self.statusCode = statusCode + self.message = intermediary.message ?? "" + self.errors = intermediary.errors ?? [] + } + + struct Intermediary: Codable { + var message: String? + var errors: [String]? + } +} + public struct Response { public let content: T public let response: URLResponse diff --git a/Sources/Buildkite/Networking/StatusCode.swift b/Sources/Buildkite/Networking/StatusCode.swift index 4554514..7f60713 100644 --- a/Sources/Buildkite/Networking/StatusCode.swift +++ b/Sources/Buildkite/Networking/StatusCode.swift @@ -12,7 +12,7 @@ import Foundation import FoundationNetworking #endif -public enum StatusCode: Int, Error { +public enum StatusCode: Int, Error, Codable { /// The request was successfully processed by Buildkite. case ok = 200 @@ -22,6 +22,9 @@ public enum StatusCode: Int, Error { /// The request has been accepted, but not yet processed. case accepted = 202 + /// The request was found + case found = 302 + /// The response to the request can be found under a different URL in the Location header and can be retrieved using a GET method on that resource. case seeOther = 303 @@ -49,8 +52,6 @@ public enum StatusCode: Int, Error { case unprocessableEntity = 422 /// The requested shop is currently locked. Shops are locked if they repeatedly exceed their API request limit, or if there is an issue with the account, such as a detected compromise or fraud risk. - /// - /// Contact support if your shop is locked. case locked = 423 /// The request was not accepted because the application has exceeded the rate limit. See the API Call Limit documentation for a breakdown of Buildkite's rate-limiting mechanism. @@ -72,5 +73,6 @@ public enum StatusCode: Int, Error { self == .ok || self == .created || self == .accepted + || self == .found } } diff --git a/Sources/Buildkite/Resources/Jobs.swift b/Sources/Buildkite/Resources/Jobs.swift index 6573485..c5d1cca 100644 --- a/Sources/Buildkite/Resources/Jobs.swift +++ b/Sources/Buildkite/Resources/Jobs.swift @@ -30,11 +30,11 @@ extension Job.Resources { public var build: Int /// job ID public var job: UUID - + public var path: String { "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/retry" } - + public func transformRequest(_ request: inout URLRequest) { request.httpMethod = "PUT" } @@ -46,7 +46,7 @@ extension Job.Resources { self.job = job } } - + /// Unblock a job /// /// Unblocks a build’s "Block pipeline" job. The job’s `unblockable` property indicates whether it is able to be unblocked, and the `unblock_url` property points to this endpoint. @@ -62,7 +62,7 @@ extension Job.Resources { public var job: UUID /// body of the request public var body: Body - + public struct Body: Codable { public var unblocker: UUID? public var fields: [String: String] @@ -72,7 +72,7 @@ extension Job.Resources { self.fields = fields } } - + public var path: String { "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/unblock" } @@ -84,15 +84,15 @@ extension Job.Resources { self.job = job self.body = body } - + public func transformRequest(_ request: inout URLRequest) { request.httpMethod = "PUT" } } - + /// Get a job’s log output - public struct LogOutput: Resource { - typealias Content = Job.LogOutput + public struct LogOutput: Resource, HasResponseBody { + public typealias Content = Job.LogOutput /// organization slug public var organization: String /// pipeline slug @@ -101,7 +101,7 @@ extension Job.Resources { public var build: Int /// job ID public var job: UUID - + public var path: String { "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log" } @@ -113,7 +113,7 @@ extension Job.Resources { self.job = job } } - + /// Delete a job’s log output public struct DeleteLogOutput: Resource { public typealias Content = Void @@ -125,7 +125,7 @@ extension Job.Resources { public var build: Int /// job ID public var job: UUID - + public var path: String { "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log" } @@ -136,12 +136,12 @@ extension Job.Resources { self.build = build self.job = job } - + public func transformRequest(_ request: inout URLRequest) { request.httpMethod = "DELETE" } } - + /// Get a job's environment variables public struct EnvironmentVariables: Resource, HasResponseBody { public typealias Content = Job.EnvironmentVariables @@ -153,7 +153,7 @@ extension Job.Resources { public var build: Int /// job ID public var job: UUID - + public var path: String { "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/env" } @@ -166,3 +166,36 @@ extension Job.Resources { } } } + +extension Job.Resources.LogOutput { + public struct Alternative: Resource, HasResponseBody { + public enum Format: String { + case html + case plainText = "txt" + } + + public typealias Content = String + /// organization slug + public var organization: String + /// pipeline slug + public var pipeline: String + /// build number + public var build: Int + /// job ID + public var job: UUID + + public var format: Format + + public var path: String { + "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log.\(format)" + } + + public init(organization: String, pipeline: String, build: Int, job: UUID, format: Format) { + self.organization = organization + self.pipeline = pipeline + self.build = build + self.job = job + self.format = format + } + } +} diff --git a/Sources/Buildkite/Resources/Teams.swift b/Sources/Buildkite/Resources/Teams.swift index b31cca6..35eb28e 100644 --- a/Sources/Buildkite/Resources/Teams.swift +++ b/Sources/Buildkite/Resources/Teams.swift @@ -21,7 +21,7 @@ extension Team.Resources { public typealias Content = [Team] /// organization slug public var organization: String - + /// Filters the results to teams that have the given user as a member. public var userId: UUID? public var path: String { diff --git a/Tests/BuildkiteTests/BuildkiteTests.swift b/Tests/BuildkiteTests/BuildkiteTests.swift index 0a3ee36..e9994b1 100644 --- a/Tests/BuildkiteTests/BuildkiteTests.swift +++ b/Tests/BuildkiteTests/BuildkiteTests.swift @@ -22,24 +22,33 @@ class BuildkiteTests: XCTestCase { private struct TestData { enum Case { case success + case successPaginated case successNoContent + case successHasBody + case successHasBodyPaginated case badResponse case unsuccessfulResponse case noData } - + var configuration = Configuration.default var client: Buildkite var resources = MockResources() - + init(testCase: Case = .success) throws { let responses: [(Data, URLResponse)] - + switch testCase { case .success: responses = [try MockData.mockingSuccess(with: resources.content, url: configuration.baseURL)] + case .successPaginated: + responses = [try MockData.mockingSuccess(with: resources.paginatedContent, url: configuration.baseURL)] case .successNoContent: responses = [MockData.mockingSuccessNoContent(url: configuration.baseURL)] + case .successHasBody: + responses = [MockData.mockingSuccessNoContent(url: configuration.baseURL)] + case .successHasBodyPaginated: + responses = [try MockData.mockingSuccess(with: resources.bodyAndPaginatedContent, url: configuration.baseURL)] case .badResponse: responses = [MockData.mockingIncompatibleResponse(for: configuration.baseURL)] case .unsuccessfulResponse: @@ -47,7 +56,7 @@ class BuildkiteTests: XCTestCase { case .noData: responses = [] } - + client = Buildkite(configuration: configuration, transport: MockTransport(responses: responses)) } @@ -59,7 +68,7 @@ class BuildkiteTests: XCTestCase { extension BuildkiteTests { func testClosureBasedRequest() throws { let testData = try TestData(testCase: .success) - + let expectation = XCTestExpectation() testData.client.send(testData.resources.contentResource) { result in do { @@ -72,17 +81,61 @@ extension BuildkiteTests { } wait(for: [expectation]) } - - func testClosureBasedRequestNoContent() throws { - let testData = try TestData(testCase: .successNoContent) - + + func testClosureBasedRequestWithPagination() throws { + let testData = try TestData(testCase: .success) + let expectation = XCTestExpectation() - testData.client.send(testData.resources.noContentResource) { _ in + testData.client.send(testData.resources.paginatedContentResource, pageOptions: PageOptions(page: 1, perPage: 30)) { result in + do { + let response = try result.get() + XCTAssertEqual(testData.resources.paginatedContent, response.content) + XCTAssertNotNil(response.page) + } catch { + XCTFail(error.localizedDescription) + } expectation.fulfill() } wait(for: [expectation]) } - + + func testClosureBasedRequestNoContent() throws { + let testData = try TestData(testCase: .successNoContent) + + let expectation = XCTestExpectation() + testData.client.send(testData.resources.noContentNoBodyResource) { _ in + expectation.fulfill() + } + wait(for: [expectation]) + } + + func testClosureBasedRequestHasBody() throws { + let testData = try TestData(testCase: .successHasBody) + + let expectation = XCTestExpectation() + testData.client.send(testData.resources.bodyResource) { _ in + expectation.fulfill() + } + wait(for: [expectation]) + } + + func testClosureBasedRequestHasBodyWithPagination() throws { + let testData = try TestData(testCase: .successHasBodyPaginated) + + let expectation = XCTestExpectation() + testData.client.send(testData.resources.bodyAndPaginatedResource, pageOptions: PageOptions(page: 1, perPage: 30)) { result in + do { + let response = try result.get() + XCTAssertEqual(testData.resources.bodyAndPaginatedContent, response.content) + XCTAssertNotNil(response.page) + } catch { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation]) + } + func testClosureBasedRequestInvalidResponse() throws { let testData = try TestData(testCase: .badResponse) let expectation = XCTestExpectation() @@ -96,7 +149,7 @@ extension BuildkiteTests { } wait(for: [expectation]) } - + func testClosureBasedRequestUnsuccessfulResponse() throws { let testData = try TestData(testCase: .unsuccessfulResponse) let expectation = XCTestExpectation() @@ -111,7 +164,7 @@ extension BuildkiteTests { } wait(for: [expectation]) } - + func testFailureFromTransport() throws { let testData = try TestData(testCase: .noData) let expectation = XCTestExpectation() @@ -148,12 +201,31 @@ extension BuildkiteTests { .store(in: &cancellables) wait(for: [expectation]) } - + + func testPublisherBasedRequestWithPagination() throws { + let testData = try TestData(testCase: .success) + let expectation = XCTestExpectation() + var cancellables: Set = [] + testData.client.sendPublisher(testData.resources.paginatedContentResource, pageOptions: PageOptions(page: 1, perPage: 30)) + .sink(receiveCompletion: { + if case let .failure(error) = $0 { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + }, + receiveValue: { + XCTAssertEqual(testData.resources.paginatedContent, $0.content) + XCTAssertNotNil($0.page) + }) + .store(in: &cancellables) + wait(for: [expectation]) + } + func testPublisherBasedRequestNoContent() throws { let testData = try TestData(testCase: .success) let expectation = XCTestExpectation() var cancellables: Set = [] - testData.client.sendPublisher(testData.resources.noContentResource) + testData.client.sendPublisher(testData.resources.noContentNoBodyResource) .sink(receiveCompletion: { if case let .failure(error) = $0 { XCTFail(error.localizedDescription) @@ -163,7 +235,41 @@ extension BuildkiteTests { .store(in: &cancellables) wait(for: [expectation]) } - + + func testPublisherBasedRequestHasBody() throws { + let testData = try TestData(testCase: .successHasBody) + let expectation = XCTestExpectation() + var cancellables: Set = [] + testData.client.sendPublisher(testData.resources.bodyResource) + .sink(receiveCompletion: { + if case let .failure(error) = $0 { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + }, receiveValue: { _ in }) + .store(in: &cancellables) + wait(for: [expectation]) + } + + func testPublisherBasedRequestHasBodyWithPagination() throws { + let testData = try TestData(testCase: .successHasBodyPaginated) + let expectation = XCTestExpectation() + var cancellables: Set = [] + testData.client.sendPublisher(testData.resources.bodyAndPaginatedResource, pageOptions: PageOptions(page: 1, perPage: 30)) + .sink(receiveCompletion: { + if case let .failure(error) = $0 { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + }, + receiveValue: { + XCTAssertEqual(testData.resources.bodyAndPaginatedContent, $0.content) + XCTAssertNotNil($0.page) + }) + .store(in: &cancellables) + wait(for: [expectation]) + } + func testPublisherBasedRequestInvalidResponse() throws { let testData = try TestData(testCase: .badResponse) let expectation = XCTestExpectation() @@ -178,7 +284,7 @@ extension BuildkiteTests { .store(in: &cancellables) wait(for: [expectation]) } - + func testPublisherBasedRequestUnsuccessfulResponse() throws { let testData = try TestData(testCase: .unsuccessfulResponse) let expectation = XCTestExpectation() diff --git a/Tests/BuildkiteTests/Networking/PaginationTests.swift b/Tests/BuildkiteTests/Networking/PaginationTests.swift deleted file mode 100644 index c8d3ab8..0000000 --- a/Tests/BuildkiteTests/Networking/PaginationTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// PaginationTests.swift -// Buildkite -// -// Created by Aaron Sky on 5/7/20. -// Copyright © 2020 Aaron Sky. All rights reserved. -// - -import Foundation -import XCTest -@testable import Buildkite - -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -class PaginationTests: XCTestCase { - func testPagination() throws { - let expected = [Pipeline(), Pipeline()] - let context = try MockContext(content: expected) - - let expectation = XCTestExpectation() - - context.client.send(Pipeline.Resources.List(organization: "buildkite"), pageOptions: PageOptions(page: 1, perPage: 30)) { result in - do { - let response = try result.get() - XCTAssertEqual(expected, response.content) - } catch { - XCTFail(error.localizedDescription) - } - expectation.fulfill() - } - wait(for: [expectation]) - } -} diff --git a/Tests/BuildkiteTests/Resources/JobsTests.swift b/Tests/BuildkiteTests/Resources/JobsTests.swift index 9a6f035..f649d29 100644 --- a/Tests/BuildkiteTests/Resources/JobsTests.swift +++ b/Tests/BuildkiteTests/Resources/JobsTests.swift @@ -55,13 +55,51 @@ class JobsTests: XCTestCase { } func testJobsLogOutput() throws { - let context = MockContext() + let expected = Job.LogOutput() + let context = try MockContext(content: expected) let expectation = XCTestExpectation() context.client.send(Job.Resources.LogOutput(organization: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())) { result in do { - _ = try result.get() + let response = try result.get() + XCTAssertEqual(expected, response.content) + } catch { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation]) + } + + func testJobsLogOutputAlternativePlainText() throws { + let expected = "hello friends" + let context = try MockContext(content: expected) + + let expectation = XCTestExpectation() + + context.client.send(Job.Resources.LogOutput.Alternative(organization: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID(), format: .plainText)) { result in + do { + let response = try result.get() + XCTAssertEqual(expected, response.content) + } catch { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation]) + } + + func testJobsLogOutputAlternativeHTML() throws { + let expected = "hello friends" + let context = try MockContext(content: expected) + + let expectation = XCTestExpectation() + + context.client.send(Job.Resources.LogOutput.Alternative(organization: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID(), format: .html)) { result in + do { + let response = try result.get() + XCTAssertEqual(expected, response.content) } catch { XCTFail(error.localizedDescription) } @@ -104,3 +142,12 @@ class JobsTests: XCTestCase { wait(for: [expectation]) } } + +extension Job.LogOutput { + init() { + self.init(url: URL(), + content: "hello friends", + size: 13, + headerTimes: []) + } +} diff --git a/Tests/BuildkiteTests/Utilities/MockData.swift b/Tests/BuildkiteTests/Utilities/MockData.swift index 71457c6..c5e5e52 100644 --- a/Tests/BuildkiteTests/Utilities/MockData.swift +++ b/Tests/BuildkiteTests/Utilities/MockData.swift @@ -14,6 +14,13 @@ import FoundationNetworking #endif struct MockResources { + struct NoContentNoBody: Resource { + typealias Content = Void + let path = "mock" + } + + var noContentNoBodyResource = NoContentNoBody() + struct HasContent: Resource, HasResponseBody { struct Content: Codable, Equatable { var name: String @@ -21,16 +28,53 @@ struct MockResources { } let path = "mock" } - + var contentResource = HasContent() var content = HasContent.Content(name: "Jeff", age: 35) - - struct NoContent: Resource { - typealias Content = Void + + struct HasPaginatedContent: Resource, Paginated { + struct Content: Codable, Equatable { + var name: String + var age: Int + } + let path = "mock" } - - var noContentResource = NoContent() + + var paginatedContentResource = HasPaginatedContent() + var paginatedContent = HasPaginatedContent.Content(name: "Jeff", age: 35) + + struct HasBody: Resource, HasRequestBody { + struct Body: Codable, Equatable { + var name: String + var age: Int + } + + var body: Body + + let path = "mock" + } + + var bodyResource = HasBody(body: HasBody.Body(name: "Jeff", age: 35)) + + struct HasBodyAndPaginated: Resource, HasRequestBody, Paginated { + struct Body: Codable, Equatable { + var name: String + var age: Int + } + + struct Content: Codable, Equatable { + var name: String + var age: Int + } + + var body: Body + + let path = "mock" + } + + var bodyAndPaginatedResource = HasBodyAndPaginated(body: HasBodyAndPaginated.Body(name: "Jeff", age: 35)) + var bodyAndPaginatedContent = HasBodyAndPaginated.Content(name: "Jeff", age: 35) } enum MockData { @@ -57,7 +101,7 @@ extension MockData { } static func mockingUnsuccessfulResponse(for url: URL) -> (Data, URLResponse) { - return (Data(), urlResponse(for: url, status: .notFound)) + return (#"{"message":"not found","errors": ["go away"]}"#.data(using: .utf8)!, urlResponse(for: url, status: .notFound)) } static func mockingSuccessNoContent(for request: URLRequest) throws -> (Data, URLResponse) { @@ -76,7 +120,7 @@ extension MockData { HTTPURLResponse(url: url, statusCode: status, httpVersion: "HTTP/1.1", - headerFields: [:])! + headerFields: ["Link":#"; rel="prev",; rel="next",; rel="first", ; rel="last""#])! } }