this repo has no description
1//
2// ATErrorTests.swift
3// CoreATProtocol
4//
5// Created by Claude on 2026-01-02.
6//
7
8import Testing
9import Foundation
10@testable import CoreATProtocol
11
12@Suite("AT Error Tests")
13struct ATErrorTests {
14
15 // MARK: - ErrorMessage Tests
16
17 @Test("ErrorMessage parses from JSON")
18 func testErrorMessageParsing() throws {
19 let json = """
20 {
21 "error": "ExpiredToken",
22 "message": "The access token has expired"
23 }
24 """.data(using: .utf8)!
25
26 let message = try JSONDecoder().decode(ErrorMessage.self, from: json)
27
28 #expect(message.error == "ExpiredToken")
29 #expect(message.message == "The access token has expired")
30 #expect(message.errorType == .expiredToken)
31 }
32
33 @Test("ErrorMessage handles unknown error types")
34 func testUnknownErrorType() throws {
35 let json = """
36 {
37 "error": "SomeNewError",
38 "message": "An unknown error occurred"
39 }
40 """.data(using: .utf8)!
41
42 let message = try JSONDecoder().decode(ErrorMessage.self, from: json)
43
44 #expect(message.error == "SomeNewError")
45 #expect(message.errorType == nil)
46 }
47
48 @Test("ErrorMessage handles missing message field")
49 func testMissingMessage() throws {
50 let json = """
51 {
52 "error": "InvalidRequest"
53 }
54 """.data(using: .utf8)!
55
56 let message = try JSONDecoder().decode(ErrorMessage.self, from: json)
57
58 #expect(message.error == "InvalidRequest")
59 #expect(message.message == nil)
60 }
61
62 // MARK: - AtErrorType Tests
63
64 @Test("All error types have descriptions")
65 func testErrorTypeDescriptions() {
66 for errorType in AtErrorType.allCases {
67 #expect(!errorType.description.isEmpty, "\(errorType) should have a description")
68 }
69 }
70
71 @Test("Error types decode correctly")
72 func testErrorTypeDecoding() throws {
73 let testCases: [(String, AtErrorType)] = [
74 ("\"AuthenticationRequired\"", .authenticationRequired),
75 ("\"ExpiredToken\"", .expiredToken),
76 ("\"RateLimitExceeded\"", .rateLimitExceeded),
77 ("\"RecordNotFound\"", .recordNotFound),
78 ("\"BlobTooLarge\"", .blobTooLarge)
79 ]
80
81 for (json, expected) in testCases {
82 let data = json.data(using: .utf8)!
83 let decoded = try JSONDecoder().decode(AtErrorType.self, from: data)
84 #expect(decoded == expected)
85 }
86 }
87
88 // MARK: - AtError Tests
89
90 @Test("AtError.requiresReauthentication identifies auth errors")
91 func testRequiresReauthentication() {
92 let expiredTokenError = AtError.message(ErrorMessage(error: "ExpiredToken", message: nil))
93 #expect(expiredTokenError.requiresReauthentication == true)
94
95 let authRequiredError = AtError.message(ErrorMessage(error: "AuthenticationRequired", message: nil))
96 #expect(authRequiredError.requiresReauthentication == true)
97
98 let notFoundError = AtError.message(ErrorMessage(error: "NotFound", message: nil))
99 #expect(notFoundError.requiresReauthentication == false)
100
101 let unauthorized = AtError.network(NetworkError.statusCode(.unauthorized, data: Data()))
102 #expect(unauthorized.requiresReauthentication == true)
103
104 let serverError = AtError.network(NetworkError.statusCode(.internalServerError, data: Data()))
105 #expect(serverError.requiresReauthentication == false)
106 }
107
108 @Test("AtError.isRetryable identifies retryable errors")
109 func testIsRetryable() {
110 let rateLimitError = AtError.message(ErrorMessage(error: "RateLimitExceeded", message: nil))
111 #expect(rateLimitError.isRetryable == true)
112
113 let serverError = AtError.network(NetworkError.statusCode(.internalServerError, data: Data()))
114 #expect(serverError.isRetryable == true)
115
116 let badRequestError = AtError.network(NetworkError.statusCode(.badRequest, data: Data()))
117 #expect(badRequestError.isRetryable == false)
118
119 let notFoundError = AtError.message(ErrorMessage(error: "NotFound", message: nil))
120 #expect(notFoundError.isRetryable == false)
121 }
122
123 // MARK: - RateLimitInfo Tests
124
125 @Test("RateLimitInfo parses from headers")
126 func testRateLimitParsing() {
127 // Create a mock response with rate limit headers
128 let url = URL(string: "https://example.com")!
129 let headers = [
130 "RateLimit-Limit": "100",
131 "RateLimit-Remaining": "50",
132 "RateLimit-Reset": "1704067200"
133 ]
134
135 let response = HTTPURLResponse(
136 url: url,
137 statusCode: 200,
138 httpVersion: nil,
139 headerFields: headers
140 )!
141
142 let rateLimitInfo = RateLimitInfo.from(response: response)
143
144 #expect(rateLimitInfo != nil)
145 #expect(rateLimitInfo?.limit == 100)
146 #expect(rateLimitInfo?.remaining == 50)
147 #expect(rateLimitInfo?.resetTimestamp == 1704067200)
148 }
149
150 @Test("RateLimitInfo returns nil for missing headers")
151 func testRateLimitMissingHeaders() {
152 let url = URL(string: "https://example.com")!
153 let response = HTTPURLResponse(
154 url: url,
155 statusCode: 200,
156 httpVersion: nil,
157 headerFields: [:]
158 )!
159
160 let rateLimitInfo = RateLimitInfo.from(response: response)
161 #expect(rateLimitInfo == nil)
162 }
163
164 @Test("RateLimitInfo calculates time until reset")
165 func testTimeUntilReset() {
166 let futureReset = Date().timeIntervalSince1970 + 300 // 5 minutes from now
167 let info = RateLimitInfo(limit: 100, remaining: 0, resetTimestamp: futureReset)
168
169 #expect(info.timeUntilReset > 0)
170 #expect(info.timeUntilReset <= 300)
171 }
172
173 // MARK: - OAuthError Tests
174
175 @Test("OAuthError has localized descriptions")
176 func testOAuthErrorDescriptions() {
177 let errors: [OAuthError] = [
178 .accessTokenExpired,
179 .refreshTokenMissing,
180 .dpopRequired,
181 .storageFailed(reason: "Test reason"),
182 .subjectMismatch(expected: "did:plc:a", received: "did:plc:b")
183 ]
184
185 for error in errors {
186 #expect(error.errorDescription != nil, "\(error) should have a description")
187 #expect(!error.errorDescription!.isEmpty)
188 }
189 }
190}