// // ATErrorTests.swift // CoreATProtocol // // Created by Claude on 2026-01-02. // import Testing import Foundation @testable import CoreATProtocol @Suite("AT Error Tests") struct ATErrorTests { // MARK: - ErrorMessage Tests @Test("ErrorMessage parses from JSON") func testErrorMessageParsing() throws { let json = """ { "error": "ExpiredToken", "message": "The access token has expired" } """.data(using: .utf8)! let message = try JSONDecoder().decode(ErrorMessage.self, from: json) #expect(message.error == "ExpiredToken") #expect(message.message == "The access token has expired") #expect(message.errorType == .expiredToken) } @Test("ErrorMessage handles unknown error types") func testUnknownErrorType() throws { let json = """ { "error": "SomeNewError", "message": "An unknown error occurred" } """.data(using: .utf8)! let message = try JSONDecoder().decode(ErrorMessage.self, from: json) #expect(message.error == "SomeNewError") #expect(message.errorType == nil) } @Test("ErrorMessage handles missing message field") func testMissingMessage() throws { let json = """ { "error": "InvalidRequest" } """.data(using: .utf8)! let message = try JSONDecoder().decode(ErrorMessage.self, from: json) #expect(message.error == "InvalidRequest") #expect(message.message == nil) } // MARK: - AtErrorType Tests @Test("All error types have descriptions") func testErrorTypeDescriptions() { for errorType in AtErrorType.allCases { #expect(!errorType.description.isEmpty, "\(errorType) should have a description") } } @Test("Error types decode correctly") func testErrorTypeDecoding() throws { let testCases: [(String, AtErrorType)] = [ ("\"AuthenticationRequired\"", .authenticationRequired), ("\"ExpiredToken\"", .expiredToken), ("\"RateLimitExceeded\"", .rateLimitExceeded), ("\"RecordNotFound\"", .recordNotFound), ("\"BlobTooLarge\"", .blobTooLarge) ] for (json, expected) in testCases { let data = json.data(using: .utf8)! let decoded = try JSONDecoder().decode(AtErrorType.self, from: data) #expect(decoded == expected) } } // MARK: - AtError Tests @Test("AtError.requiresReauthentication identifies auth errors") func testRequiresReauthentication() { let expiredTokenError = AtError.message(ErrorMessage(error: "ExpiredToken", message: nil)) #expect(expiredTokenError.requiresReauthentication == true) let authRequiredError = AtError.message(ErrorMessage(error: "AuthenticationRequired", message: nil)) #expect(authRequiredError.requiresReauthentication == true) let notFoundError = AtError.message(ErrorMessage(error: "NotFound", message: nil)) #expect(notFoundError.requiresReauthentication == false) let unauthorized = AtError.network(NetworkError.statusCode(.unauthorized, data: Data())) #expect(unauthorized.requiresReauthentication == true) let serverError = AtError.network(NetworkError.statusCode(.internalServerError, data: Data())) #expect(serverError.requiresReauthentication == false) } @Test("AtError.isRetryable identifies retryable errors") func testIsRetryable() { let rateLimitError = AtError.message(ErrorMessage(error: "RateLimitExceeded", message: nil)) #expect(rateLimitError.isRetryable == true) let serverError = AtError.network(NetworkError.statusCode(.internalServerError, data: Data())) #expect(serverError.isRetryable == true) let badRequestError = AtError.network(NetworkError.statusCode(.badRequest, data: Data())) #expect(badRequestError.isRetryable == false) let notFoundError = AtError.message(ErrorMessage(error: "NotFound", message: nil)) #expect(notFoundError.isRetryable == false) } // MARK: - RateLimitInfo Tests @Test("RateLimitInfo parses from headers") func testRateLimitParsing() { // Create a mock response with rate limit headers let url = URL(string: "https://example.com")! let headers = [ "RateLimit-Limit": "100", "RateLimit-Remaining": "50", "RateLimit-Reset": "1704067200" ] let response = HTTPURLResponse( url: url, statusCode: 200, httpVersion: nil, headerFields: headers )! let rateLimitInfo = RateLimitInfo.from(response: response) #expect(rateLimitInfo != nil) #expect(rateLimitInfo?.limit == 100) #expect(rateLimitInfo?.remaining == 50) #expect(rateLimitInfo?.resetTimestamp == 1704067200) } @Test("RateLimitInfo returns nil for missing headers") func testRateLimitMissingHeaders() { let url = URL(string: "https://example.com")! let response = HTTPURLResponse( url: url, statusCode: 200, httpVersion: nil, headerFields: [:] )! let rateLimitInfo = RateLimitInfo.from(response: response) #expect(rateLimitInfo == nil) } @Test("RateLimitInfo calculates time until reset") func testTimeUntilReset() { let futureReset = Date().timeIntervalSince1970 + 300 // 5 minutes from now let info = RateLimitInfo(limit: 100, remaining: 0, resetTimestamp: futureReset) #expect(info.timeUntilReset > 0) #expect(info.timeUntilReset <= 300) } // MARK: - OAuthError Tests @Test("OAuthError has localized descriptions") func testOAuthErrorDescriptions() { let errors: [OAuthError] = [ .accessTokenExpired, .refreshTokenMissing, .dpopRequired, .storageFailed(reason: "Test reason"), .subjectMismatch(expected: "did:plc:a", received: "did:plc:b") ] for error in errors { #expect(error.errorDescription != nil, "\(error) should have a description") #expect(!error.errorDescription!.isEmpty) } } }