Embed an iOS and Catalyst share extension using existing discovery and player APIs. Share saved players through an App Group snapshot, preserve the queue, and verify advancing playback before reporting success. Cover URL provider transport, settings migration, discovery addresses, duplicate handling, cancellation, and partial enqueue/play outcomes with focused tests. Document authentication and signing limits.
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() {}
|
|
}
|