migration

This commit is contained in:
Daniel Arantes Loverde
2026-08-11 13:22:02 -03:00
parent c4d6998595
commit 7702836fe7
231 changed files with 4329 additions and 1958 deletions

View File

@@ -0,0 +1,182 @@
import Foundation
import Testing
@testable import PediFoods
private func makeClient() -> ApiClient {
ApiClient(session: URLProtocolStub.makeSession(), tokenStore: FakeTokenStore())
}
private final class FakeTokenStore: TokenStore {
var jwt: String?
func clear() { jwt = nil }
}
// MARK: - shouldRetry
@Test("shouldRetry is true for rate limiting and transport errors")
func shouldRetryTrueForRateLimitAndTransport() {
let client = makeClient()
#expect(client.shouldRetry(.rateLimited(nil)))
#expect(client.shouldRetry(.transportError("timeout")))
}
@Test("shouldRetry is true only for 5xx http errors, not 4xx")
func shouldRetryOnlyFor5xx() {
let client = makeClient()
#expect(client.shouldRetry(.httpError(500, nil)))
#expect(client.shouldRetry(.httpError(503, nil)))
#expect(client.shouldRetry(.httpError(404, nil)) == false)
#expect(client.shouldRetry(.httpError(400, nil)) == false)
}
@Test("shouldRetry is false for terminal client-side errors")
func shouldRetryFalseForTerminalErrors() {
let client = makeClient()
#expect(client.shouldRetry(.invalidURL) == false)
#expect(client.shouldRetry(.unauthorized(nil)) == false)
#expect(client.shouldRetry(.cancelled) == false)
#expect(client.shouldRetry(.decodeError(nil)) == false)
}
// MARK: - backoff
@Test("backoff honors a rate-limit response's Retry-After value in nanoseconds")
func backoffHonorsRetryAfter() {
let client = makeClient()
#expect(client.backoff(for: 1, error: .rateLimited(2)) == 2_000_000_000)
}
@Test("backoff scales with attempt number and is capped at 2 seconds")
func backoffScalesAndCaps() {
let client = makeClient()
let first = client.backoff(for: 1, error: .transportError("x"))
let second = client.backoff(for: 2, error: .transportError("x"))
#expect(second > first)
#expect(client.backoff(for: 100, error: .transportError("x")) == 2_000_000_000)
}
// MARK: - buildURL
@Test("buildURL appends the path to the base URL and preserves query items")
func buildURLAppendsPathAndQuery() throws {
let client = makeClient()
let base = URL(string: "https://api.example.com")!
let url = try client.buildURL(path: "/stores/1", query: [URLQueryItem(name: "lang", value: "pt")], baseURL: base)
#expect(url.absoluteString == "https://api.example.com/stores/1?lang=pt")
}
@Test("buildURL omits the query string entirely when there are no query items")
func buildURLOmitsEmptyQuery() throws {
let client = makeClient()
let url = try client.buildURL(path: "/stores/1", query: [], baseURL: URL(string: "https://api.example.com")!)
#expect(url.absoluteString == "https://api.example.com/stores/1")
}
// MARK: - buildHeaders
@Test("buildHeaders sends the customer JWT as a Bearer token when the request requires auth")
func buildHeadersUsesCustomerJWT() {
let store = FakeTokenStore()
store.jwt = "customer-jwt"
let client = ApiClient(session: URLProtocolStub.makeSession(), tokenStore: store)
let request = ApiRequest(path: "/me", requiresAuth: true)
#expect(client.buildHeaders(for: request)["Authorization"] == "Bearer customer-jwt")
}
@Test("buildHeaders prefers an explicit customBearerToken over the customer JWT")
func buildHeadersPrefersCustomBearerToken() {
let store = FakeTokenStore()
store.jwt = "customer-jwt"
let client = ApiClient(session: URLProtocolStub.makeSession(), tokenStore: store)
let request = ApiRequest(path: "/guest", requiresAuth: true, customBearerToken: "guest-token")
#expect(client.buildHeaders(for: request)["Authorization"] == "Bearer guest-token")
}
@Test("buildHeaders omits Authorization when the request doesn't require auth and has no custom token")
func buildHeadersOmitsAuthWhenNotRequired() {
let client = makeClient()
let request = ApiRequest(path: "/public", requiresAuth: false)
#expect(client.buildHeaders(for: request)["Authorization"] == nil)
}
@Test("buildHeaders adds the module-specific Atomenta-Token when the module has one")
func buildHeadersAddsModuleToken() {
let client = makeClient()
let request = ApiRequest(path: "/stores", module: .store)
#expect(client.buildHeaders(for: request)["Atomenta-Token"] == ApiConfig.storeToken)
}
// MARK: - isSessionExpiredPayload
@Test("isSessionExpiredPayload recognizes auth/token/unauthorized error codes")
func isSessionExpiredPayloadRecognizesCodes() {
let client = makeClient()
#expect(client.isSessionExpiredPayload(code: "AUTH_INVALID", message: nil))
#expect(client.isSessionExpiredPayload(code: "token_expired", message: nil))
#expect(client.isSessionExpiredPayload(code: "UNAUTHORIZED", message: nil))
#expect(client.isSessionExpiredPayload(code: "NOT_FOUND", message: nil) == false)
}
@Test("isSessionExpiredPayload recognizes a message mentioning both token and expired/invalid/session")
func isSessionExpiredPayloadRecognizesMessages() {
let client = makeClient()
#expect(client.isSessionExpiredPayload(code: nil, message: "Token expired, please log in again"))
#expect(client.isSessionExpiredPayload(code: nil, message: "Invalid token"))
#expect(client.isSessionExpiredPayload(code: nil, message: "Token session invalid"))
#expect(client.isSessionExpiredPayload(code: nil, message: "Something else went wrong") == false)
#expect(client.isSessionExpiredPayload(code: nil, message: "Token is fine, no issues here") == false)
}
// MARK: - sanitizedMessage
@Test("sanitizedMessage replaces a data:image payload with a generic message")
func sanitizedMessageReplacesDataImage() {
let client = makeClient()
#expect(client.sanitizedMessage("data:image/png;base64,abc123") == "Erro ao processar imagem.")
}
@Test("sanitizedMessage replaces any base64 payload with a generic message")
func sanitizedMessageReplacesBase64Payload() {
let client = makeClient()
#expect(client.sanitizedMessage("field: base64,abc123def") == "Resposta do servidor inválida.")
}
@Test("sanitizedMessage truncates long strings to 300 characters with an ellipsis")
func sanitizedMessageTruncatesLongStrings() {
let client = makeClient()
let long = String(repeating: "x", count: 500)
let sanitized = client.sanitizedMessage(long)
#expect(sanitized?.count == 301) // 300 chars + the ellipsis character
#expect(sanitized?.hasSuffix("") == true)
}
@Test("sanitizedMessage returns nil for an empty or whitespace-only string")
func sanitizedMessageNilForEmptyString() {
let client = makeClient()
#expect(client.sanitizedMessage(" ") == nil)
}
// MARK: - serverPayload(from: Data)
@Test("serverPayload extracts code and message from an ApiEnvelope-shaped error response")
func serverPayloadExtractsFromEnvelope() {
let client = makeClient()
let data = Data(#"{"error":true,"code":"NOT_FOUND","message":"Store not found"}"#.utf8)
let payload = client.serverPayload(from: data)
#expect(payload?.code == "NOT_FOUND")
#expect(payload?.message == "Store not found")
}
@Test("serverPayload falls back to a loose JSON object when it isn't a full envelope")
func serverPayloadFallsBackToLooseObject() {
let client = makeClient()
let data = Data(#"{"msg":"Something failed"}"#.utf8)
#expect(client.serverPayload(from: data)?.message == "Something failed")
}
@Test("serverPayload returns nil for a response with no error signal at all")
func serverPayloadNilForNoErrorSignal() {
let client = makeClient()
let data = Data(#"{"id":"1","name":"OK"}"#.utf8)
#expect(client.serverPayload(from: data) == nil)
}