Files
yattee/YatteeTests/NetworkingTests.swift
2026-02-08 18:33:56 +01:00

305 lines
12 KiB
Swift

//
// NetworkingTests.swift
// YatteeTests
//
// Tests for networking layer components.
//
import Testing
import Foundation
@testable import Yattee
// MARK: - Endpoint Tests
@Suite("Endpoint Tests")
@MainActor
struct EndpointTests {
@Test("GET endpoint construction")
func getEndpoint() {
let endpoint = GenericEndpoint.get("/api/v1/videos")
#expect(endpoint.path == "/api/v1/videos")
#expect(endpoint.method == .get)
#expect(endpoint.queryItems == nil)
#expect(endpoint.body == nil)
}
@Test("GET endpoint with query parameters")
func getEndpointWithQuery() {
let endpoint = GenericEndpoint.get("/api/v1/search", query: [
"q": "test",
"page": "1"
])
#expect(endpoint.path == "/api/v1/search")
#expect(endpoint.queryItems?.count == 2)
let queryDict = Dictionary(uniqueKeysWithValues: endpoint.queryItems!.map { ($0.name, $0.value) })
#expect(queryDict["q"] == "test")
#expect(queryDict["page"] == "1")
}
@Test("POST endpoint with body")
func postEndpointWithBody() throws {
struct TestBody: Encodable {
let name: String
}
let body = TestBody(name: "test")
let endpoint = GenericEndpoint.post("/api/v1/create", body: body)
#expect(endpoint.path == "/api/v1/create")
#expect(endpoint.method == .post)
#expect(endpoint.body != nil)
}
@Test("Generic endpoint with custom timeout")
func endpointWithTimeout() {
let endpoint = GenericEndpoint(path: "/slow", timeout: 60)
#expect(endpoint.timeout == 60)
}
@Test("Default endpoint timeout is 30 seconds")
func defaultTimeout() {
let endpoint = GenericEndpoint.get("/fast")
#expect(endpoint.timeout == 30)
}
}
// MARK: - APIError Tests
@Suite("APIError Tests")
@MainActor
struct APIErrorTests {
@Test("APIError descriptions")
func errorDescriptions() {
let invalidURL = APIError.invalidURL
#expect(invalidURL.localizedDescription.contains("URL"))
let httpError = APIError.httpError(statusCode: 404, message: nil)
#expect(httpError.localizedDescription.contains("404"))
let timeout = APIError.timeout
#expect(timeout.localizedDescription.contains("timed out"))
let notFound = APIError.notFound(nil)
#expect(notFound.localizedDescription.contains("not found"))
}
@Test("All simple error descriptions")
func allSimpleErrorDescriptions() {
#expect(APIError.invalidURL.errorDescription == "Invalid URL")
#expect(APIError.timeout.errorDescription == "Request timed out")
#expect(APIError.noConnection.errorDescription == "No network connection")
#expect(APIError.cancelled.errorDescription == "Request was cancelled")
#expect(APIError.unauthorized.errorDescription == "Authentication required")
#expect(APIError.notFound(nil).errorDescription == "Resource not found")
#expect(APIError.commentsDisabled.errorDescription == "Comments are disabled")
#expect(APIError.noInstance.errorDescription == "No suitable instance available")
#expect(APIError.noStreams.errorDescription == "No playable streams available")
#expect(APIError.invalidRequest.errorDescription == "Invalid request")
}
@Test("Decoding error description")
func decodingErrorDescription() {
let error = APIError.decodingError("Missing key 'title'")
#expect(error.errorDescription?.contains("Missing key 'title'") == true)
}
@Test("Server error description")
func serverErrorDescription() {
let error = APIError.serverError("Internal server error")
#expect(error.errorDescription?.contains("Internal server error") == true)
}
@Test("Rate limited description with retry after")
func rateLimitedWithRetry() {
let error = APIError.rateLimited(retryAfter: 60)
#expect(error.errorDescription?.contains("60") == true)
}
@Test("Rate limited description without retry after")
func rateLimitedWithoutRetry() {
let error = APIError.rateLimited(retryAfter: nil)
#expect(error.errorDescription == "Rate limited")
}
@Test("Unknown error description")
func unknownErrorDescription() {
let error = APIError.unknown("Something went wrong")
#expect(error.errorDescription == "Something went wrong")
}
@Test("APIError equality")
func errorEquality() {
#expect(APIError.invalidURL == APIError.invalidURL)
#expect(APIError.httpError(statusCode: 404, message: nil) == APIError.httpError(statusCode: 404, message: nil))
#expect(APIError.httpError(statusCode: 404, message: nil) != APIError.httpError(statusCode: 500, message: nil))
#expect(APIError.timeout == APIError.timeout)
#expect(APIError.notFound(nil) == APIError.notFound(nil))
#expect(APIError.unauthorized == APIError.unauthorized)
}
@Test("APIError equality for parameterized errors")
func parameterizedErrorEquality() {
#expect(APIError.decodingError("msg") == APIError.decodingError("msg"))
#expect(APIError.decodingError("msg1") != APIError.decodingError("msg2"))
#expect(APIError.serverError("msg") == APIError.serverError("msg"))
#expect(APIError.serverError("msg1") != APIError.serverError("msg2"))
#expect(APIError.rateLimited(retryAfter: 30) == APIError.rateLimited(retryAfter: 30))
#expect(APIError.rateLimited(retryAfter: nil) == APIError.rateLimited(retryAfter: nil))
#expect(APIError.rateLimited(retryAfter: 30) != APIError.rateLimited(retryAfter: 60))
#expect(APIError.unknown("msg") == APIError.unknown("msg"))
#expect(APIError.unknown("msg1") != APIError.unknown("msg2"))
#expect(APIError.notFound(nil) == APIError.notFound(nil))
#expect(APIError.notFound("detail") == APIError.notFound("detail"))
#expect(APIError.notFound("detail1") != APIError.notFound("detail2"))
#expect(APIError.notFound(nil) != APIError.notFound("detail"))
}
@Test("notFound error with detail message")
func notFoundWithDetail() {
let noDetail = APIError.notFound(nil)
#expect(noDetail.errorDescription == "Resource not found")
let withDetail = APIError.notFound("Video not found: This live event will begin in 11 days.")
#expect(withDetail.errorDescription == "Video not found: This live event will begin in 11 days.")
}
@Test("Different error types are not equal")
func differentTypesNotEqual() {
#expect(APIError.invalidURL != APIError.timeout)
#expect(APIError.notFound(nil) != APIError.unauthorized)
#expect(APIError.commentsDisabled != APIError.noStreams)
}
@Test("APIError isRetryable")
func retryableErrors() {
#expect(APIError.timeout.isRetryable == true)
#expect(APIError.noConnection.isRetryable == true)
#expect(APIError.rateLimited(retryAfter: 60).isRetryable == true)
#expect(APIError.httpError(statusCode: 500, message: nil).isRetryable == true)
#expect(APIError.httpError(statusCode: 429, message: nil).isRetryable == true)
#expect(APIError.invalidURL.isRetryable == false)
#expect(APIError.notFound(nil).isRetryable == false)
#expect(APIError.unauthorized.isRetryable == false)
#expect(APIError.httpError(statusCode: 400, message: nil).isRetryable == false)
}
@Test("All non-retryable errors")
func allNonRetryableErrors() {
#expect(APIError.invalidURL.isRetryable == false)
#expect(APIError.decodingError("").isRetryable == false)
#expect(APIError.cancelled.isRetryable == false)
#expect(APIError.serverError("").isRetryable == false)
#expect(APIError.unauthorized.isRetryable == false)
#expect(APIError.notFound(nil).isRetryable == false)
#expect(APIError.commentsDisabled.isRetryable == false)
#expect(APIError.noInstance.isRetryable == false)
#expect(APIError.noStreams.isRetryable == false)
#expect(APIError.invalidRequest.isRetryable == false)
#expect(APIError.unknown("").isRetryable == false)
}
@Test("Server errors (5xx) are retryable")
func serverErrorsRetryable() {
#expect(APIError.httpError(statusCode: 500, message: nil).isRetryable == true)
#expect(APIError.httpError(statusCode: 502, message: nil).isRetryable == true)
#expect(APIError.httpError(statusCode: 503, message: nil).isRetryable == true)
#expect(APIError.httpError(statusCode: 504, message: nil).isRetryable == true)
}
@Test("Decoding error from Swift DecodingError types")
func decodingErrorFactory() {
// Test typeMismatch
let typeMismatchContext = DecodingError.Context(codingPath: [], debugDescription: "Expected String")
let typeMismatch = DecodingError.typeMismatch(String.self, typeMismatchContext)
let apiError1 = APIError.decodingError(typeMismatch)
#expect(apiError1.errorDescription?.contains("Type mismatch") == true)
// Test valueNotFound
let valueNotFoundContext = DecodingError.Context(codingPath: [], debugDescription: "No value")
let valueNotFound = DecodingError.valueNotFound(Int.self, valueNotFoundContext)
let apiError2 = APIError.decodingError(valueNotFound)
#expect(apiError2.errorDescription?.contains("Value not found") == true)
// Test dataCorrupted
let dataCorruptedContext = DecodingError.Context(codingPath: [], debugDescription: "Corrupted")
let dataCorrupted = DecodingError.dataCorrupted(dataCorruptedContext)
let apiError3 = APIError.decodingError(dataCorrupted)
#expect(apiError3.errorDescription?.contains("Data corrupted") == true)
}
}
// MARK: - URL Building Tests
@Suite("URL Building Tests")
@MainActor
struct URLBuildingTests {
@Test("Build URL from base and endpoint")
func buildURL() throws {
let baseURL = URL(string: "https://api.example.com")!
let endpoint = GenericEndpoint.get("/v1/videos")
let request = try endpoint.urlRequest(baseURL: baseURL)
#expect(request.url?.absoluteString == "https://api.example.com/v1/videos")
}
@Test("Build URL with query parameters")
func buildURLWithQuery() throws {
let baseURL = URL(string: "https://api.example.com")!
let endpoint = GenericEndpoint.get("/search", query: [
"q": "hello world",
"limit": "10"
])
let request = try endpoint.urlRequest(baseURL: baseURL)
let urlString = request.url?.absoluteString ?? ""
#expect(urlString.contains("q=hello%20world"))
#expect(urlString.contains("limit=10"))
}
@Test("URLRequest has correct method")
func requestMethod() throws {
let baseURL = URL(string: "https://api.example.com")!
let getEndpoint = GenericEndpoint.get("/resource")
let getRequest = try getEndpoint.urlRequest(baseURL: baseURL)
#expect(getRequest.httpMethod == "GET")
let postEndpoint = GenericEndpoint.post("/resource", body: ["key": "value"])
let postRequest = try postEndpoint.urlRequest(baseURL: baseURL)
#expect(postRequest.httpMethod == "POST")
}
@Test("URLRequest has JSON Accept header")
func acceptHeader() throws {
let baseURL = URL(string: "https://api.example.com")!
let endpoint = GenericEndpoint.get("/resource")
let request = try endpoint.urlRequest(baseURL: baseURL)
#expect(request.value(forHTTPHeaderField: "Accept") == "application/json")
}
}
// MARK: - HTTPMethod Tests
@Suite("HTTPMethod Tests")
@MainActor
struct HTTPMethodTests {
@Test("HTTPMethod raw values")
func rawValues() {
#expect(HTTPMethod.get.rawValue == "GET")
#expect(HTTPMethod.post.rawValue == "POST")
#expect(HTTPMethod.put.rawValue == "PUT")
#expect(HTTPMethod.patch.rawValue == "PATCH")
#expect(HTTPMethod.delete.rawValue == "DELETE")
}
}