import Foundation import XCTest import VelodyDomain @testable import VelodyNetworking final class URLSessionVelodyAPIClientAuthorizationTests: XCTestCase { override func tearDown() { RecordingURLProtocol.handler = nil super.tearDown() } func testFetchRemoteLibrarySendsAuthorizationHeaderWhenDeviceTokenIsAvailable() async throws { RecordingURLProtocol.handler = { request in XCTAssertEqual( request.value(forHTTPHeaderField: "Authorization"), "Bearer test-device-access-token" ) XCTAssertEqual(request.url?.path, "/api/v1/library/tracks") XCTAssertEqual(request.url?.query, "deviceId=device-123") return ( HTTPURLResponse( url: try XCTUnwrap(request.url), statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": "application/json"] )!, Data(#"{"tracks":[]}"#.utf8) ) } let client = URLSessionVelodyAPIClient( environment: ServerEnvironment( baseURL: URL(string: "http://127.0.0.1:3007")!, appVersion: "Tests" ), session: makeSession(), deviceAccessTokenProvider: { "test-device-access-token" } ) let response = try await client.fetchRemoteLibrary(deviceId: "device-123") XCTAssertEqual(response.tracks.count, 0) } func testFetchSyncChangesSendsAuthorizationHeaderAndCursorQuery() async throws { RecordingURLProtocol.handler = { request in XCTAssertEqual( request.value(forHTTPHeaderField: "Authorization"), "Bearer test-device-access-token" ) XCTAssertEqual(request.url?.path, "/api/v1/sync/changes") XCTAssertEqual(request.url?.query, "cursor=12") return ( HTTPURLResponse( url: try XCTUnwrap(request.url), statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": "application/json"] )!, Data( """ { "nextCursor": "12", "hasMore": false, "requiresBootstrap": false, "events": [], "serverTime": "2026-06-15T12:00:00.000Z" } """.utf8 ) ) } let client = URLSessionVelodyAPIClient( environment: ServerEnvironment( baseURL: URL(string: "http://127.0.0.1:3007")!, appVersion: "Tests" ), session: makeSession(), deviceAccessTokenProvider: { "test-device-access-token" } ) let response = try await client.fetchSyncChanges(cursor: SyncCursor(value: "12")) XCTAssertEqual(response.nextCursor.value, "12") XCTAssertEqual(response.events.count, 0) } func testRegisterDeviceDoesNotSendAuthorizationHeader() async throws { RecordingURLProtocol.handler = { request in XCTAssertNil(request.value(forHTTPHeaderField: "Authorization")) XCTAssertEqual(request.url?.path, "/api/v1/devices/register") return ( HTTPURLResponse( url: try XCTUnwrap(request.url), statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": "application/json"] )!, Data( """ { "deviceId": "device-123", "deviceAccessToken": "registered-device-token", "bootstrapToken": "bootstrap-token", "serverTime": "2026-06-09T10:00:00.000Z" } """.utf8 ) ) } let client = URLSessionVelodyAPIClient( environment: ServerEnvironment( baseURL: URL(string: "http://127.0.0.1:3007")!, appVersion: "Tests" ), session: makeSession(), deviceAccessTokenProvider: { "should-not-be-sent" } ) let response = try await client.registerDevice( DeviceRegistrationPayload( platform: .iphone, deviceName: "Test iPhone", appVersion: "Tests" ) ) XCTAssertEqual(response.deviceId, "device-123") XCTAssertEqual(response.deviceAccessToken, "registered-device-token") } private func makeSession() -> URLSession { let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [RecordingURLProtocol.self] return URLSession(configuration: configuration) } } private final class RecordingURLProtocol: URLProtocol { static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override class func canInit(with request: URLRequest) -> Bool { request.url?.host == "127.0.0.1" } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { guard let handler = Self.handler else { XCTFail("RecordingURLProtocol.handler must be set before use.") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } override func stopLoading() {} }