Added support for Buildkite's JSON errors, and wrote more tests for pagination

This commit is contained in:
Aaron Sky 2020-05-08 10:06:46 -04:00
parent 2865672821
commit 9481c26290
11 changed files with 312 additions and 115 deletions

View File

@ -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. 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. 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.

View File

@ -49,7 +49,7 @@ public final class Buildkite {
transport.send(request: request, completion: handleContentfulResponse(completion: completion)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
} }
public func send<R: Resource & HasResponseBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) { public func send<R: Resource & Paginated>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) {
let request = URLRequest(resource, configuration: configuration, pageOptions: pageOptions) let request = URLRequest(resource, configuration: configuration, pageOptions: pageOptions)
transport.send(request: request, completion: handleContentfulResponse(completion: completion)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
} }
@ -65,7 +65,7 @@ public final class Buildkite {
transport.send(request: request, completion: handleContentfulResponse(completion: completion)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
} }
public func send<R: Resource & HasRequestBody & HasResponseBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) { public func send<R: Resource & HasRequestBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) {
let request: URLRequest let request: URLRequest
do { do {
request = try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) 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)) transport.send(request: request, completion: handleEmptyResponse(completion: completion))
} }
public func send<R: Resource & HasRequestBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<Void>, 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<Content: Decodable>(completion: @escaping (Result<Response<Content>, Error>) -> Void) -> Transport.Completion { private func handleContentfulResponse<Content: Decodable>(completion: @escaping (Result<Response<Content>, Error>) -> Void) -> Transport.Completion {
return { [weak self] result in return { [weak self] result in
guard let self = self else { guard let self = self else {
@ -113,7 +102,7 @@ public final class Buildkite {
do { do {
let data: Data let data: Data
(data, response) = try result.get() (data, response) = try result.get()
try self.checkResponseForIssues(response) try self.checkResponseForIssues(response, data: data)
content = try self.decoder.decode(Content.self, from: data) content = try self.decoder.decode(Content.self, from: data)
} catch { } catch {
completion(.failure(error)) 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, guard let response = response as? HTTPURLResponse,
let statusCode = StatusCode(rawValue: response.statusCode) else { let statusCode = StatusCode(rawValue: response.statusCode) else {
throw ResponseError.missingResponse throw ResponseError.missingResponse
} }
if !statusCode.isSuccess { 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<R: Resource & HasResponseBody>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> { public func sendPublisher<R: Resource & HasResponseBody>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> {
transport.sendPublisher(request: URLRequest(resource, configuration: configuration)) transport.sendPublisher(request: URLRequest(resource, configuration: configuration))
.tryMap { .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) let content = try self.decoder.decode(R.Content.self, from: $0.data)
return Response(content: content, response: $0.response) return Response(content: content, response: $0.response)
} }
@ -169,7 +163,7 @@ extension Buildkite {
public func sendPublisher<R: Resource & HasResponseBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> { public func sendPublisher<R: Resource & HasResponseBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> {
transport.sendPublisher(request: URLRequest(resource, configuration: configuration, pageOptions: pageOptions)) transport.sendPublisher(request: URLRequest(resource, configuration: configuration, pageOptions: pageOptions))
.tryMap { .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) let content = try self.decoder.decode(R.Content.self, from: $0.data)
return Response(content: content, response: $0.response) return Response(content: content, response: $0.response)
} }
@ -181,19 +175,19 @@ extension Buildkite {
.publisher .publisher
.flatMap(transport.sendPublisher) .flatMap(transport.sendPublisher)
.tryMap { .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) let content = try self.decoder.decode(R.Content.self, from: $0.data)
return Response(content: content, response: $0.response) return Response(content: content, response: $0.response)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
public func sendPublisher<R: Resource & HasRequestBody & HasResponseBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> { public func sendPublisher<R: Resource & HasRequestBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> {
Result { try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) } Result { try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) }
.publisher .publisher
.flatMap(transport.sendPublisher) .flatMap(transport.sendPublisher)
.tryMap { .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) let content = try self.decoder.decode(R.Content.self, from: $0.data)
return Response(content: content, response: $0.response) return Response(content: content, response: $0.response)
} }
@ -219,16 +213,5 @@ extension Buildkite {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func sendPublisher<R: Resource & HasRequestBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<Void>, 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 #endif

View File

@ -127,7 +127,7 @@ public enum Job: Codable, Equatable {
} }
} }
public struct LogOutput: Codable { public struct LogOutput: Codable, Equatable {
public var url: URL // Resource<Job.Resources.GetLogOutput> public var url: URL // Resource<Job.Resources.GetLogOutput>
public var content: String public var content: String
public var size: Int public var size: Int

View File

@ -17,6 +17,23 @@ enum ResponseError: Error {
case unexpectedlyNoContent 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<T> { public struct Response<T> {
public let content: T public let content: T
public let response: URLResponse public let response: URLResponse

View File

@ -12,7 +12,7 @@ import Foundation
import FoundationNetworking import FoundationNetworking
#endif #endif
public enum StatusCode: Int, Error { public enum StatusCode: Int, Error, Codable {
/// The request was successfully processed by Buildkite. /// The request was successfully processed by Buildkite.
case ok = 200 case ok = 200
@ -22,6 +22,9 @@ public enum StatusCode: Int, Error {
/// The request has been accepted, but not yet processed. /// The request has been accepted, but not yet processed.
case accepted = 202 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. /// 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 case seeOther = 303
@ -49,8 +52,6 @@ public enum StatusCode: Int, Error {
case unprocessableEntity = 422 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. /// 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 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. /// 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 == .ok
|| self == .created || self == .created
|| self == .accepted || self == .accepted
|| self == .found
} }
} }

View File

@ -30,11 +30,11 @@ extension Job.Resources {
public var build: Int public var build: Int
/// job ID /// job ID
public var job: UUID public var job: UUID
public var path: String { public var path: String {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/retry" "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/retry"
} }
public func transformRequest(_ request: inout URLRequest) { public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "PUT" request.httpMethod = "PUT"
} }
@ -46,7 +46,7 @@ extension Job.Resources {
self.job = job self.job = job
} }
} }
/// Unblock a job /// Unblock a job
/// ///
/// Unblocks a builds "Block pipeline" job. The jobs `unblockable` property indicates whether it is able to be unblocked, and the `unblock_url` property points to this endpoint. /// Unblocks a builds "Block pipeline" job. The jobs `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 public var job: UUID
/// body of the request /// body of the request
public var body: Body public var body: Body
public struct Body: Codable { public struct Body: Codable {
public var unblocker: UUID? public var unblocker: UUID?
public var fields: [String: String] public var fields: [String: String]
@ -72,7 +72,7 @@ extension Job.Resources {
self.fields = fields self.fields = fields
} }
} }
public var path: String { public var path: String {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/unblock" "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/unblock"
} }
@ -84,15 +84,15 @@ extension Job.Resources {
self.job = job self.job = job
self.body = body self.body = body
} }
public func transformRequest(_ request: inout URLRequest) { public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "PUT" request.httpMethod = "PUT"
} }
} }
/// Get a jobs log output /// Get a jobs log output
public struct LogOutput: Resource { public struct LogOutput: Resource, HasResponseBody {
typealias Content = Job.LogOutput public typealias Content = Job.LogOutput
/// organization slug /// organization slug
public var organization: String public var organization: String
/// pipeline slug /// pipeline slug
@ -101,7 +101,7 @@ extension Job.Resources {
public var build: Int public var build: Int
/// job ID /// job ID
public var job: UUID public var job: UUID
public var path: String { public var path: String {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log" "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log"
} }
@ -113,7 +113,7 @@ extension Job.Resources {
self.job = job self.job = job
} }
} }
/// Delete a jobs log output /// Delete a jobs log output
public struct DeleteLogOutput: Resource { public struct DeleteLogOutput: Resource {
public typealias Content = Void public typealias Content = Void
@ -125,7 +125,7 @@ extension Job.Resources {
public var build: Int public var build: Int
/// job ID /// job ID
public var job: UUID public var job: UUID
public var path: String { public var path: String {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log" "organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log"
} }
@ -136,12 +136,12 @@ extension Job.Resources {
self.build = build self.build = build
self.job = job self.job = job
} }
public func transformRequest(_ request: inout URLRequest) { public func transformRequest(_ request: inout URLRequest) {
request.httpMethod = "DELETE" request.httpMethod = "DELETE"
} }
} }
/// Get a job's environment variables /// Get a job's environment variables
public struct EnvironmentVariables: Resource, HasResponseBody { public struct EnvironmentVariables: Resource, HasResponseBody {
public typealias Content = Job.EnvironmentVariables public typealias Content = Job.EnvironmentVariables
@ -153,7 +153,7 @@ extension Job.Resources {
public var build: Int public var build: Int
/// job ID /// job ID
public var job: UUID public var job: UUID
public var path: String { public var path: String {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/env" "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
}
}
}

View File

@ -21,7 +21,7 @@ extension Team.Resources {
public typealias Content = [Team] public typealias Content = [Team]
/// organization slug /// organization slug
public var organization: String public var organization: String
/// Filters the results to teams that have the given user as a member.
public var userId: UUID? public var userId: UUID?
public var path: String { public var path: String {

View File

@ -22,24 +22,33 @@ class BuildkiteTests: XCTestCase {
private struct TestData { private struct TestData {
enum Case { enum Case {
case success case success
case successPaginated
case successNoContent case successNoContent
case successHasBody
case successHasBodyPaginated
case badResponse case badResponse
case unsuccessfulResponse case unsuccessfulResponse
case noData case noData
} }
var configuration = Configuration.default var configuration = Configuration.default
var client: Buildkite var client: Buildkite
var resources = MockResources() var resources = MockResources()
init(testCase: Case = .success) throws { init(testCase: Case = .success) throws {
let responses: [(Data, URLResponse)] let responses: [(Data, URLResponse)]
switch testCase { switch testCase {
case .success: case .success:
responses = [try MockData.mockingSuccess(with: resources.content, url: configuration.baseURL)] responses = [try MockData.mockingSuccess(with: resources.content, url: configuration.baseURL)]
case .successPaginated:
responses = [try MockData.mockingSuccess(with: resources.paginatedContent, url: configuration.baseURL)]
case .successNoContent: case .successNoContent:
responses = [MockData.mockingSuccessNoContent(url: configuration.baseURL)] 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: case .badResponse:
responses = [MockData.mockingIncompatibleResponse(for: configuration.baseURL)] responses = [MockData.mockingIncompatibleResponse(for: configuration.baseURL)]
case .unsuccessfulResponse: case .unsuccessfulResponse:
@ -47,7 +56,7 @@ class BuildkiteTests: XCTestCase {
case .noData: case .noData:
responses = [] responses = []
} }
client = Buildkite(configuration: configuration, client = Buildkite(configuration: configuration,
transport: MockTransport(responses: responses)) transport: MockTransport(responses: responses))
} }
@ -59,7 +68,7 @@ class BuildkiteTests: XCTestCase {
extension BuildkiteTests { extension BuildkiteTests {
func testClosureBasedRequest() throws { func testClosureBasedRequest() throws {
let testData = try TestData(testCase: .success) let testData = try TestData(testCase: .success)
let expectation = XCTestExpectation() let expectation = XCTestExpectation()
testData.client.send(testData.resources.contentResource) { result in testData.client.send(testData.resources.contentResource) { result in
do { do {
@ -72,17 +81,61 @@ extension BuildkiteTests {
} }
wait(for: [expectation]) wait(for: [expectation])
} }
func testClosureBasedRequestNoContent() throws { func testClosureBasedRequestWithPagination() throws {
let testData = try TestData(testCase: .successNoContent) let testData = try TestData(testCase: .success)
let expectation = XCTestExpectation() 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() expectation.fulfill()
} }
wait(for: [expectation]) 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 { func testClosureBasedRequestInvalidResponse() throws {
let testData = try TestData(testCase: .badResponse) let testData = try TestData(testCase: .badResponse)
let expectation = XCTestExpectation() let expectation = XCTestExpectation()
@ -96,7 +149,7 @@ extension BuildkiteTests {
} }
wait(for: [expectation]) wait(for: [expectation])
} }
func testClosureBasedRequestUnsuccessfulResponse() throws { func testClosureBasedRequestUnsuccessfulResponse() throws {
let testData = try TestData(testCase: .unsuccessfulResponse) let testData = try TestData(testCase: .unsuccessfulResponse)
let expectation = XCTestExpectation() let expectation = XCTestExpectation()
@ -111,7 +164,7 @@ extension BuildkiteTests {
} }
wait(for: [expectation]) wait(for: [expectation])
} }
func testFailureFromTransport() throws { func testFailureFromTransport() throws {
let testData = try TestData(testCase: .noData) let testData = try TestData(testCase: .noData)
let expectation = XCTestExpectation() let expectation = XCTestExpectation()
@ -148,12 +201,31 @@ extension BuildkiteTests {
.store(in: &cancellables) .store(in: &cancellables)
wait(for: [expectation]) wait(for: [expectation])
} }
func testPublisherBasedRequestWithPagination() throws {
let testData = try TestData(testCase: .success)
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
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 { func testPublisherBasedRequestNoContent() throws {
let testData = try TestData(testCase: .success) let testData = try TestData(testCase: .success)
let expectation = XCTestExpectation() let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = [] var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.noContentResource) testData.client.sendPublisher(testData.resources.noContentNoBodyResource)
.sink(receiveCompletion: { .sink(receiveCompletion: {
if case let .failure(error) = $0 { if case let .failure(error) = $0 {
XCTFail(error.localizedDescription) XCTFail(error.localizedDescription)
@ -163,7 +235,41 @@ extension BuildkiteTests {
.store(in: &cancellables) .store(in: &cancellables)
wait(for: [expectation]) wait(for: [expectation])
} }
func testPublisherBasedRequestHasBody() throws {
let testData = try TestData(testCase: .successHasBody)
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
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<AnyCancellable> = []
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 { func testPublisherBasedRequestInvalidResponse() throws {
let testData = try TestData(testCase: .badResponse) let testData = try TestData(testCase: .badResponse)
let expectation = XCTestExpectation() let expectation = XCTestExpectation()
@ -178,7 +284,7 @@ extension BuildkiteTests {
.store(in: &cancellables) .store(in: &cancellables)
wait(for: [expectation]) wait(for: [expectation])
} }
func testPublisherBasedRequestUnsuccessfulResponse() throws { func testPublisherBasedRequestUnsuccessfulResponse() throws {
let testData = try TestData(testCase: .unsuccessfulResponse) let testData = try TestData(testCase: .unsuccessfulResponse)
let expectation = XCTestExpectation() let expectation = XCTestExpectation()

View File

@ -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])
}
}

View File

@ -55,13 +55,51 @@ class JobsTests: XCTestCase {
} }
func testJobsLogOutput() throws { func testJobsLogOutput() throws {
let context = MockContext() let expected = Job.LogOutput()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation() let expectation = XCTestExpectation()
context.client.send(Job.Resources.LogOutput(organization: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())) { result in context.client.send(Job.Resources.LogOutput(organization: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())) { result in
do { 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 { } catch {
XCTFail(error.localizedDescription) XCTFail(error.localizedDescription)
} }
@ -104,3 +142,12 @@ class JobsTests: XCTestCase {
wait(for: [expectation]) wait(for: [expectation])
} }
} }
extension Job.LogOutput {
init() {
self.init(url: URL(),
content: "hello friends",
size: 13,
headerTimes: [])
}
}

View File

@ -14,6 +14,13 @@ import FoundationNetworking
#endif #endif
struct MockResources { struct MockResources {
struct NoContentNoBody: Resource {
typealias Content = Void
let path = "mock"
}
var noContentNoBodyResource = NoContentNoBody()
struct HasContent: Resource, HasResponseBody { struct HasContent: Resource, HasResponseBody {
struct Content: Codable, Equatable { struct Content: Codable, Equatable {
var name: String var name: String
@ -21,16 +28,53 @@ struct MockResources {
} }
let path = "mock" let path = "mock"
} }
var contentResource = HasContent() var contentResource = HasContent()
var content = HasContent.Content(name: "Jeff", age: 35) var content = HasContent.Content(name: "Jeff", age: 35)
struct NoContent: Resource { struct HasPaginatedContent: Resource, Paginated {
typealias Content = Void struct Content: Codable, Equatable {
var name: String
var age: Int
}
let path = "mock" 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 { enum MockData {
@ -57,7 +101,7 @@ extension MockData {
} }
static func mockingUnsuccessfulResponse(for url: URL) -> (Data, URLResponse) { 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) { static func mockingSuccessNoContent(for request: URLRequest) throws -> (Data, URLResponse) {
@ -76,7 +120,7 @@ extension MockData {
HTTPURLResponse(url: url, HTTPURLResponse(url: url,
statusCode: status, statusCode: status,
httpVersion: "HTTP/1.1", httpVersion: "HTTP/1.1",
headerFields: [:])! headerFields: ["Link":#"<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=3>; rel="prev",<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=5>; rel="next",<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=1>; rel="first", <https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=10>; rel="last""#])!
} }
} }