114 lines
4.6 KiB
Swift
114 lines
4.6 KiB
Swift
import Foundation
|
|||
|
|
import XCTest
|
||
|
|
@testable import QueueCubeShareCore
|
||
|
|
|
||
|
|
final class RequestBuilderTests: XCTestCase {
|
||
|
|
override func setUp() {
|
||
|
|
super.setUp()
|
||
|
|
URLProtocol.registerClass(StubURLProtocol.self)
|
||
|
|
}
|
||
|
|
|
||
|
|
override func tearDown() {
|
||
|
|
StubURLProtocol.setResponse(status: 200, body: Data())
|
||
|
|
URLProtocol.unregisterClass(StubURLProtocol.self)
|
||
|
|
super.tearDown()
|
||
|
|
}
|
||
|
|
|
||
|
|
func testConfiguredTimeoutAndBodySurviveEndpointBuilding() throws {
|
||
|
|
let baseURL = URL(string: "https://queuecube-test.invalid/api")!
|
||
|
|
let request = RequestBuilder(url: baseURL, timeout: 7)
|
||
|
|
.path("/playlist")
|
||
|
|
.body(["url": "https://video.example/watch?v=one"])
|
||
|
|
.method(.post)
|
||
|
|
.build()
|
||
|
|
|
||
|
|
XCTAssertEqual(request.url?.path, "/api/playlist")
|
||
|
|
XCTAssertEqual(request.timeoutInterval, 7)
|
||
|
|
XCTAssertEqual(request.httpMethod, "POST")
|
||
|
|
XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json")
|
||
|
|
XCTAssertEqual(request.value(forHTTPHeaderField: "Cache-Control"), "no-cache")
|
||
|
|
XCTAssertEqual(try JSONDecoder().decode([String: String].self, from: XCTUnwrap(request.httpBody)), ["url": "https://video.example/watch?v=one"])
|
||
|
|
XCTAssertEqual(RequestBuilder(url: baseURL, timeout: 7).pathString("/search?q=one").build().timeoutInterval, 7)
|
||
|
|
}
|
||
|
|
|
||
|
|
func testPOSTRejectsAuthenticationFailureInsteadOfReportingSuccess() async throws {
|
||
|
|
StubURLProtocol.setResponse(status: 401, body: Data(#"{"error":"Authentication required"}"#.utf8))
|
||
|
|
do {
|
||
|
|
try await RequestBuilder(url: URL(string: "https://queuecube-test.invalid/api/playlist")!).post()
|
||
|
|
XCTFail("A non-2xx mutation must fail")
|
||
|
|
} catch RequestBuilder.RequestError.httpError(let status, let message) {
|
||
|
|
XCTAssertEqual(status, 401)
|
||
|
|
XCTAssertEqual(message, "Authentication required")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testNonJSONServerFailureStillReportsItsStatus() async throws {
|
||
|
|
StubURLProtocol.setResponse(status: 502, body: Data("upstream unavailable".utf8))
|
||
|
|
do {
|
||
|
|
try await RequestBuilder(url: URL(string: "https://queuecube-test.invalid/api/play")!).post()
|
||
|
|
XCTFail("A non-JSON server failure must fail")
|
||
|
|
} catch RequestBuilder.RequestError.httpError(let status, let message) {
|
||
|
|
XCTAssertEqual(status, 502)
|
||
|
|
XCTAssertNil(message)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func testSuccessfulEmptyMutationResponseIsAccepted() async throws {
|
||
|
|
StubURLProtocol.setResponse(status: 204, body: Data())
|
||
|
|
try await RequestBuilder(url: URL(string: "https://queuecube-test.invalid/api/play")!).post()
|
||
|
|
}
|
||
|
|
|
||
|
|
func testAPIReadsExistingPlaylistEndpointAndDecodesIDs() async throws {
|
||
|
|
StubURLProtocol.setResponse(status: 200, body: Data(#"[{"id":42,"filename":"https://video.example/one"}]"#.utf8))
|
||
|
|
let api = API(baseURL: URL(string: "https://queuecube-test.invalid/api")!, requestTimeout: 7)
|
||
|
|
let playlist = try await api.fetchPlaylist()
|
||
|
|
XCTAssertEqual(playlist.map(\.id), [42])
|
||
|
|
let request = try XCTUnwrap(StubURLProtocol.lastRequest())
|
||
|
|
XCTAssertEqual(request.url?.path, "/api/playlist")
|
||
|
|
XCTAssertEqual(request.httpMethod, "GET")
|
||
|
|
XCTAssertEqual(request.timeoutInterval, 7)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Intercepts only this test's reserved host. No network or real player is used.
|
||
|
|
private final class StubURLProtocol: URLProtocol {
|
||
|
|
private static let lock = NSLock()
|
||
|
|
private static var status = 200
|
||
|
|
private static var body = Data()
|
||
|
|
private static var request: URLRequest?
|
||
|
|
|
||
|
|
static func setResponse(status: Int, body: Data) {
|
||
|
|
lock.lock()
|
||
|
|
defer { lock.unlock() }
|
||
|
|
Self.status = status
|
||
|
|
Self.body = body
|
||
|
|
Self.request = nil
|
||
|
|
}
|
||
|
|
|
||
|
|
static func lastRequest() -> URLRequest? {
|
||
|
|
lock.lock()
|
||
|
|
defer { lock.unlock() }
|
||
|
|
return request
|
||
|
|
}
|
||
|
|
|
||
|
|
override class func canInit(with request: URLRequest) -> Bool {
|
||
|
|
request.url?.host == "queuecube-test.invalid"
|
||
|
|
}
|
||
|
|
|
||
|
|
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
||
|
|
|
||
|
|
override func startLoading() {
|
||
|
|
Self.lock.lock()
|
||
|
|
let status = Self.status
|
||
|
|
let body = Self.body
|
||
|
|
Self.request = request
|
||
|
|
Self.lock.unlock()
|
||
|
|
let response = HTTPURLResponse(url: request.url!, statusCode: status, httpVersion: "HTTP/1.1", headerFields: [:])!
|
||
|
|
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||
|
|
client?.urlProtocol(self, didLoad: body)
|
||
|
|
client?.urlProtocolDidFinishLoading(self)
|
||
|
|
}
|
||
|
|
|
||
|
|
override func stopLoading() {}
|
||
|
|
}
|