migration
This commit is contained in:
182
PediFoodsTests/ApiClientTests.swift
Normal file
182
PediFoodsTests/ApiClientTests.swift
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user