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() {} }