import Foundation import Testing @testable import Haven final class MockURLProtocol: URLProtocol, @unchecked Sendable { nonisolated(unsafe) static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { guard let handler = Self.requestHandler else { client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse)) 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() {} } @Suite("Wallhaven API", .serialized) @MainActor struct WallhavenAPITests { private func makeAPI() -> WallhavenAPI { let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [MockURLProtocol.self] return WallhavenAPI(session: URLSession(configuration: configuration)) } private func response(for request: URLRequest, statusCode: Int = 200) -> HTTPURLResponse { HTTPURLResponse( url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"] )! } @Test("Search sends every selected filter and decodes the response") func searchBuildsExpectedRequest() async throws { MockURLProtocol.requestHandler = { request in let url = try #require(request.url) let components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false)) let items = Dictionary( uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value ?? "") }) #expect(components.path == "/api/v1/search") #expect(items["q"] == "mountain lake") #expect(items["categories"] == "101") #expect(items["purity"] == "110") #expect(items["sorting"] == "toplist") #expect(items["resolutions"] == "1920x1080,3840x2160") #expect(items["ratios"] == "16x9,21x9") #expect(items["limit"] == "12") #expect(items["page"] == "3") return (response(for: request), try encodedSearchResponse(currentPage: 3, lastPage: 9)) } let (wallpapers, meta) = try await makeAPI().searchWallpapers( query: "mountain lake", categories: "101", purity: "110", sorting: .toplist, resolutions: ["1920x1080", "3840x2160"], ratios: ["16x9", "21x9"], limit: 12, page: 3 ) #expect(wallpapers == [makeWallpaper()]) #expect(meta?.current_page == 3) #expect(meta?.last_page == 9) } @Test("Empty filters are omitted and defaults are stable") func emptyFiltersAreOmitted() async throws { MockURLProtocol.requestHandler = { request in let url = try #require(request.url) let components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false)) let items = Dictionary( uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value ?? "") }) #expect(items["categories"] == "111") #expect(items["purity"] == "100") #expect(items["sorting"] == "date_added") #expect(items["limit"] == "24") #expect(items["page"] == "1") #expect(items["resolutions"] == nil) #expect(items["ratios"] == nil) return (response(for: request), try encodedSearchResponse()) } _ = try await makeAPI().searchWallpapers(query: "") } @Test("Random mode maps to the random sorting query") func randomSortingIsEncoded() async throws { MockURLProtocol.requestHandler = { request in let url = try #require(request.url) let components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false)) #expect(components.queryItems?.first(where: { $0.name == "sorting" })?.value == "random") return (response(for: request), try encodedSearchResponse()) } _ = try await makeAPI().searchWallpapers(query: "", sorting: .random) } @Test("Non-success HTTP responses become API errors") func nonSuccessStatusThrowsAPIError() async { MockURLProtocol.requestHandler = { request in (response(for: request, statusCode: 503), Data("unavailable".utf8)) } do { _ = try await makeAPI().searchWallpapers(query: "") Issue.record("Expected a WallhavenError.apiError") } catch WallhavenError.apiError { // Expected. } catch { Issue.record("Unexpected error: \(error)") } } @Test("Malformed success payloads become decoding errors") func malformedPayloadThrowsDecodingError() async { MockURLProtocol.requestHandler = { request in (response(for: request), Data(#"{"unexpected":true}"#.utf8)) } do { _ = try await makeAPI().searchWallpapers(query: "") Issue.record("Expected a WallhavenError.decodingError") } catch WallhavenError.decodingError { // Expected. } catch { Issue.record("Unexpected error: \(error)") } } @Test("Transport failures propagate to the caller") func transportFailurePropagates() async { MockURLProtocol.requestHandler = { _ in throw URLError(.notConnectedToInternet) } do { _ = try await makeAPI().searchWallpapers(query: "") Issue.record("Expected a transport error") } catch let error as URLError { #expect(error.code == .notConnectedToInternet) } catch { Issue.record("Unexpected error: \(error)") } } }