Files
steipete--codexbar/Tests/CodexBarTests/MoonshotUsageFetcherTests.swift
2026-07-13 12:22:33 +08:00

221 lines
7.0 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
@Suite(.serialized)
struct MoonshotUsageFetcherTests {
@Test
func `parses documented response`() throws {
let json = """
{
"code": 0,
"data": {
"available_balance": 49.58,
"voucher_balance": 50.00,
"cash_balance": 12.34
},
"scode": "0x0",
"status": true
}
"""
let summary = try MoonshotUsageFetcher._parseSummaryForTesting(Data(json.utf8))
#expect(summary.availableBalance == 49.58)
#expect(summary.voucherBalance == 50.00)
#expect(summary.cashBalance == 12.34)
let usage = MoonshotUsageSnapshot(summary: summary).toUsageSnapshot()
#expect(usage.primary == nil)
#expect(usage.secondary == nil)
#expect(usage.loginMethod(for: .moonshot) == "Balance: $49.58")
}
@Test
func `negative cash balance is surfaced as deficit`() throws {
let json = """
{
"code": 0,
"data": {
"available_balance": 49.58,
"voucher_balance": 50.00,
"cash_balance": -0.42
},
"scode": "0x0",
"status": true
}
"""
let summary = try MoonshotUsageFetcher._parseSummaryForTesting(Data(json.utf8))
let usage = MoonshotUsageSnapshot(summary: summary).toUsageSnapshot()
#expect(summary.cashBalance == -0.42)
#expect(usage.loginMethod(for: .moonshot)?.contains("in deficit") == true)
}
@Test
func `invalid root returns parse error`() {
let json = """
[{ "available_balance": 1 }]
"""
#expect {
_ = try MoonshotUsageFetcher._parseSummaryForTesting(Data(json.utf8))
} throws: { error in
guard case MoonshotUsageError.parseFailed = error else { return false }
return true
}
}
@Test
func `api code failure returns api error`() {
let json = """
{
"code": 401,
"data": {
"available_balance": 0,
"voucher_balance": 0,
"cash_balance": 0
},
"scode": "unauthorized",
"status": false
}
"""
#expect {
_ = try MoonshotUsageFetcher._parseSummaryForTesting(Data(json.utf8))
} throws: { error in
guard case let MoonshotUsageError.apiError(message) = error else { return false }
return message == "code 401, scode unauthorized"
}
}
@Test
func `international host uses moonshot ai`() {
let url = MoonshotUsageFetcher.resolveBalanceURL(region: .international)
#expect(url.absoluteString == "https://api.moonshot.ai/v1/users/me/balance")
}
@Test
func `china host uses moonshot cn`() {
let url = MoonshotUsageFetcher.resolveBalanceURL(region: .china)
#expect(url.absoluteString == "https://api.moonshot.cn/v1/users/me/balance")
}
@Test
func `fetch usage sends bearer token and bounded request`() async throws {
defer {
MoonshotStubURLProtocol.requests = []
MoonshotStubURLProtocol.handler = nil
}
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MoonshotStubURLProtocol.self]
let session = URLSession(configuration: config)
MoonshotStubURLProtocol.requests = []
MoonshotStubURLProtocol.handler = { request in
let url = try #require(request.url)
#expect(url.absoluteString == "https://api.moonshot.cn/v1/users/me/balance")
#expect(request.httpMethod == "GET")
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer live-token")
#expect(request.value(forHTTPHeaderField: "Accept") == "application/json")
#expect(request.timeoutInterval == 15)
let body = """
{
"code": 0,
"data": {
"available_balance": 9.87,
"voucher_balance": 1.23,
"cash_balance": 8.64
},
"scode": "0x0",
"status": true
}
"""
let response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: nil,
headerFields: ["Content-Type": "application/json"])!
return (response, Data(body.utf8))
}
let snapshot = try await MoonshotUsageFetcher.fetchUsage(
apiKey: " live-token ",
region: .china,
session: session)
#expect(MoonshotStubURLProtocol.requests.count == 1)
#expect(snapshot.summary.availableBalance == 9.87)
#expect(snapshot.toUsageSnapshot().loginMethod(for: .moonshot) == "Balance: $9.87")
}
@Test
func `fetch usage surfaces http failure without leaking body`() async throws {
defer {
MoonshotStubURLProtocol.requests = []
MoonshotStubURLProtocol.handler = nil
}
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MoonshotStubURLProtocol.self]
let session = URLSession(configuration: config)
MoonshotStubURLProtocol.requests = []
MoonshotStubURLProtocol.handler = { request in
let url = try #require(request.url)
let response = HTTPURLResponse(
url: url,
statusCode: 401,
httpVersion: nil,
headerFields: ["Content-Type": "application/json"])!
return (response, Data(#"{"error":"secret-ish provider body"}"#.utf8))
}
await #expect {
_ = try await MoonshotUsageFetcher.fetchUsage(
apiKey: "live-token",
session: session)
} throws: { error in
guard case let MoonshotUsageError.apiError(message) = error else { return false }
return message == "HTTP 401"
}
}
}
final class MoonshotStubURLProtocol: URLProtocol {
nonisolated(unsafe) static var requests: [URLRequest] = []
nonisolated(unsafe) static var handler: (@Sendable (URLRequest) throws -> (HTTPURLResponse, Data))?
override static func canInit(with _: URLRequest) -> Bool {
true
}
override static func canonicalRequest(for request: URLRequest) -> URLRequest {
request
}
override func startLoading() {
Self.requests.append(self.request)
guard let handler = Self.handler else {
self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
return
}
do {
let (response, data) = try handler(self.request)
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
self.client?.urlProtocol(self, didLoad: data)
self.client?.urlProtocolDidFinishLoading(self)
} catch {
self.client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}