498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
110 lines
3.6 KiB
Swift
110 lines
3.6 KiB
Swift
// AccountSnapshotLoaderTests.swift
|
|
// OSGKeyboardTests
|
|
//
|
|
// Resilient account aggregation keeps cached optional data when a secondary
|
|
// endpoint is temporarily unavailable.
|
|
|
|
@testable import OSGKeyboard
|
|
@testable import OSGKeyboardHostSupport
|
|
import XCTest
|
|
|
|
final class AccountSnapshotLoaderTests: XCTestCase {
|
|
@MainActor
|
|
func testOptionalReferralFailuresUseCachedAccountData() async throws {
|
|
let account = OSGKeyboard.AccountSession(
|
|
accountID: UUID(),
|
|
createdAtEpochSeconds: 1_700_000_000
|
|
)
|
|
let cachedReferral = AccountReferral(
|
|
id: UUID(),
|
|
status: .rewarded,
|
|
createdAtEpochSeconds: 1_700_000_100,
|
|
rewardCredits: 1_000
|
|
)
|
|
let cached = AccountCenterSnapshot(
|
|
account: account,
|
|
credits: AccountCreditSummary(balance: 1_000, usedCredits: 0),
|
|
referralProfile: AccountReferralProfile(
|
|
code: "CachedCode_1234567890",
|
|
boundCode: nil,
|
|
inviterRewardCredits: 1_000,
|
|
inviteeRewardCredits: 1_000
|
|
),
|
|
referrals: [cachedReferral]
|
|
)
|
|
let transport = AccountCenterRoutingTransport(accountID: account.accountID)
|
|
let client = AccountAPIClient(
|
|
baseURL: URL(string: "https://account.test")!,
|
|
transport: transport,
|
|
sessionVault: InMemoryAccountSecurityStore(
|
|
session: makeAccountSession()
|
|
)
|
|
)
|
|
let loader = AccountCenterSnapshotLoader(apiClient: client)
|
|
|
|
let refreshed = try await loader.load(cachedSnapshot: cached)
|
|
|
|
XCTAssertEqual(
|
|
refreshed.credits,
|
|
AccountCreditSummary(balance: 750, usedCredits: 250)
|
|
)
|
|
XCTAssertEqual(refreshed.referralProfile, cached.referralProfile)
|
|
XCTAssertEqual(refreshed.referrals, cached.referrals)
|
|
let requests = await transport.requests
|
|
XCTAssertEqual(
|
|
requests.count { $0.url?.path == "/v1/referrals/me" },
|
|
2
|
|
)
|
|
XCTAssertEqual(
|
|
requests.count { $0.url?.path == "/v1/referrals" },
|
|
2
|
|
)
|
|
}
|
|
}
|
|
|
|
private actor AccountCenterRoutingTransport: AccountHTTPTransport {
|
|
let accountID: UUID
|
|
private(set) var requests: [URLRequest] = []
|
|
|
|
init(accountID: UUID) {
|
|
self.accountID = accountID
|
|
}
|
|
|
|
func data(for request: URLRequest) async throws -> (Data, HTTPURLResponse) {
|
|
requests.append(request)
|
|
let response: QueueAccountTransport.Stub
|
|
switch request.url?.path {
|
|
case "/v1/account":
|
|
response = .init(
|
|
statusCode: 200,
|
|
body: Data(
|
|
"""
|
|
{"data":{"id":"\(accountID.uuidString)","createdAtEpochSeconds":1700000000}}
|
|
""".utf8
|
|
)
|
|
)
|
|
case "/v1/credits/balance":
|
|
response = .init(
|
|
statusCode: 200,
|
|
body: Data(#"{"balance":750,"lifetimeUsed":250}"#.utf8)
|
|
)
|
|
case "/v1/referrals/me", "/v1/referrals":
|
|
response = .init(
|
|
statusCode: 503,
|
|
body: apiErrorData(
|
|
code: "external_service_unavailable",
|
|
message: "temporarily unavailable"
|
|
)
|
|
)
|
|
case "/v1/referrals/campaigns":
|
|
response = .init(statusCode: 200, body: Data("[]".utf8))
|
|
default:
|
|
throw AccountAPIError.transport
|
|
}
|
|
return (
|
|
response.body,
|
|
makeHTTPResponse(request: request, statusCode: response.statusCode)
|
|
)
|
|
}
|
|
}
|