codeburn/mac/Tests/CodeBurnMenubarTests/CommandCodeQuotaTests.swift
Resham Joshi 4c779ef301
fix(commandcode): show the monthly window (#1734)
Command Code's credits endpoint only reports monthly credits left, so the
monthly window was dropped. For an active subscription on a known plan the
plan price now comes from a small table (the API has no monthly cap field):
pool = max(plan, left), used = pool - left, reset = currentPeriodEnd.
Unknown plans and purchased/free credits stay out. CLI and menubar mirrored.
2026-10-10 21:40:43 +02:00

128 lines
6.4 KiB
Swift

import Foundation
import Testing
@testable import CodeBurnMenubar
@Suite("Command Code quota")
@MainActor
struct CommandCodeQuotaTests {
private final class RequestRecorder: @unchecked Sendable {
private(set) var requests: [URLRequest] = []
func record(_ request: URLRequest) { requests.append(request) }
}
nonisolated private static let creditsBody = """
{"credits":{"monthlyCredits":3.9706645399,"purchasedCredits":0,"freeCredits":0},
"windowLimits":{"limited":true,"exceeded":"weekly",
"fiveHour":{"used":0.75,"cap":3,"exceeded":false,"resetAt":0},
"weekly":{"used":6.0293354601,"cap":6,"exceeded":true,"resetAt":1791653049551}}}
"""
nonisolated private static let subscriptionBody = #"{"success":true,"data":{"status":"active","planId":"individual-go-v1","currentPeriodEnd":"2026-11-01T10:38:29.000Z"}}"#
nonisolated private static func response(_ request: URLRequest, _ status: Int) -> HTTPURLResponse {
HTTPURLResponse(url: request.url!, statusCode: status, httpVersion: nil, headerFields: nil)!
}
private static func deps(
key: String? = "synthetic-commandcode-key",
recorder: RequestRecorder = RequestRecorder(),
respond: @escaping @Sendable (URLRequest) throws -> (Data, HTTPURLResponse)
) -> CommandCodeSubscriptionService.Deps {
CommandCodeSubscriptionService.Deps(
loadAPIKey: { key },
fetch: { request in
recorder.record(request)
return try respond(request)
}
)
}
@Test("maps both windows, the plan and the remaining credits")
func decodesLiveShape() async throws {
let recorder = RequestRecorder()
let summary = try await CommandCodeSubscriptionService.refresh(deps: Self.deps(recorder: recorder) { request in
let body = request.url == CommandCodeSubscriptionService.creditsURL ? Self.creditsBody : Self.subscriptionBody
return (Data(body.utf8), Self.response(request, 200))
})
#expect(summary.details.map(\.label) == ["5-hour", "Weekly", "Monthly"])
#expect(summary.details[0].percent == 0.25)
#expect(summary.details[1].percent == 1)
#expect(abs(summary.details[2].percent - 0.6029) < 0.0001)
#expect(summary.details[0].resetsAt == nil)
#expect(summary.details[1].resetsAt == Date(timeIntervalSince1970: 1_791_653_049.551))
#expect(summary.details[2].resetsAt == Date(timeIntervalSince1970: 1_793_529_509))
#expect(summary.primary?.label == "Weekly")
#expect(summary.planLabel == "Go")
#expect(summary.footerLines == ["Credits left: $3.97 monthly"])
#expect(recorder.requests.count == 2)
for request in recorder.requests {
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer synthetic-commandcode-key")
#expect(request.value(forHTTPHeaderField: "User-Agent") == "CodeBurn")
}
}
@Test("the monthly window needs an active subscription on a known plan")
func monthlyNeedsKnownActivePlan() throws {
let body = Data(Self.creditsBody.utf8)
let known = CommandCodeSubscriptionService.Subscription(
planID: "individual-go-v1", status: "active", currentPeriodEnd: "2026-11-01T10:38:29Z")
let go = try CommandCodeSubscriptionService.decode(body, subscription: known)
#expect(go.details.last?.label == "Monthly")
#expect(go.details.last?.resetsAt == Date(timeIntervalSince1970: 1_793_529_509))
#expect(go.primary?.label == "Weekly")
var unknown = known
unknown.planID = "individual-mystery"
#expect(try CommandCodeSubscriptionService.decode(body, subscription: unknown).details.map(\.label) == ["5-hour", "Weekly"])
var inactive = known
inactive.status = "canceled"
#expect(try CommandCodeSubscriptionService.decode(body, subscription: inactive).details.map(\.label) == ["5-hour", "Weekly"])
}
@Test("a failed subscription call only drops the plan label")
func subscriptionFailureKeepsWindows() async throws {
let summary = try await CommandCodeSubscriptionService.refresh(deps: Self.deps { request in
request.url == CommandCodeSubscriptionService.creditsURL
? (Data(Self.creditsBody.utf8), Self.response(request, 200))
: (Data("{}".utf8), Self.response(request, 500))
})
#expect(summary.planLabel == nil)
#expect(summary.details.count == 2)
}
@Test("missing key, rejected key and bad bodies map to their errors")
func errors() async {
let noKey = Self.deps(key: nil) { _ in
Issue.record("must not fetch without a key")
throw URLError(.badURL)
}
await #expect(throws: CommandCodeSubscriptionService.FetchError.noCredentials) {
try await CommandCodeSubscriptionService.refresh(deps: noKey)
}
let cases: [(Int, String, CommandCodeSubscriptionService.FetchError)] = [
(401, "{}", .authenticationRejected),
(429, "{}", .rateLimited),
(503, "{}", .providerUnavailable),
(200, #"{"windowLimits":{}}"#, .parseFailure),
]
for (status, body, expected) in cases {
await #expect(throws: expected) {
try await CommandCodeSubscriptionService.refresh(deps: Self.deps { request in
(Data(body.utf8), Self.response(request, status))
})
}
}
await #expect(throws: CommandCodeSubscriptionService.FetchError.network) {
try await CommandCodeSubscriptionService.refresh(deps: Self.deps { _ in throw URLError(.notConnectedToInternet) })
}
}
@Test("plan labels and the auth file")
func planLabelAndAuthFile() throws {
#expect(CommandCodeSubscriptionService.planLabel("individual-pro-v1") == "Pro")
#expect(CommandCodeSubscriptionService.planLabel("") == nil)
let file = FileManager.default.temporaryDirectory.appendingPathComponent("cc-auth-\(UUID().uuidString).json")
defer { try? FileManager.default.removeItem(at: file) }
try Data(#"{"apiKey":" synthetic ","userName":"x"}"#.utf8).write(to: file)
#expect(CommandCodeSubscriptionService.apiKey(fromAuthFile: file) == "synthetic")
#expect(CommandCodeSubscriptionService.apiKey(fromAuthFile: file.appendingPathExtension("missing")) == nil)
}
}