1243 lines
46 KiB
Swift
1243 lines
46 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
struct MiniMaxAPISettingsReaderTests {
|
|
@Test
|
|
func `api token prefers coding plan specific environment key`() {
|
|
let token = MiniMaxAPISettingsReader.apiToken(environment: [
|
|
"MINIMAX_API_KEY": "sk-api-standard",
|
|
"MINIMAX_CODING_API_KEY": "sk-cp-coding-plan",
|
|
])
|
|
|
|
#expect(token == "sk-cp-coding-plan")
|
|
#expect(MiniMaxAPISettingsReader.apiKeyKind(token: token) == .codingPlan)
|
|
}
|
|
|
|
@Test
|
|
func `api token falls back to generic environment key`() {
|
|
let token = MiniMaxAPISettingsReader.apiToken(environment: [
|
|
"MINIMAX_API_KEY": "\"sk-api-standard\"",
|
|
])
|
|
|
|
#expect(token == "sk-api-standard")
|
|
#expect(MiniMaxAPISettingsReader.apiKeyKind(token: token) == .standard)
|
|
}
|
|
}
|
|
|
|
struct MiniMaxEndpointOverrideSettingsTests {
|
|
@Test
|
|
func `strict endpoint overrides reject non MiniMax hosts`() {
|
|
let env = [
|
|
MiniMaxSettingsReader.requireProviderEndpointOverridesKey: "true",
|
|
MiniMaxSettingsReader.hostKey: "https://attacker.example",
|
|
MiniMaxSettingsReader.codingPlanURLKey: "https://attacker.example/coding-plan",
|
|
MiniMaxSettingsReader.remainsURLKey: "https://attacker.example/remains",
|
|
MiniMaxSettingsReader.billingHistoryURLKey: "https://attacker.example/account/amount",
|
|
]
|
|
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.codingPlanURL(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.billingHistoryURL(environment: env) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `endpoint overrides reject encoded host delimiters before suffix matching`() {
|
|
let encodedSlash = "https://attacker.example%2f.platform.minimax.io"
|
|
let doubleEncodedSlash = "https://attacker.example%252f.platform.minimax.io"
|
|
let env = [
|
|
MiniMaxSettingsReader.hostKey: encodedSlash,
|
|
MiniMaxSettingsReader.codingPlanURLKey: "\(encodedSlash)/coding-plan",
|
|
MiniMaxSettingsReader.remainsURLKey: "\(encodedSlash)/remains",
|
|
MiniMaxSettingsReader.billingHistoryURLKey: "\(encodedSlash)/account/amount",
|
|
]
|
|
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.codingPlanURL(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.billingHistoryURL(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: [
|
|
MiniMaxSettingsReader.hostKey: doubleEncodedSlash,
|
|
]) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `endpoint overrides reject whitespace and control characters in hosts`() {
|
|
for host in ["https://bad host", "https://bad%20host", "https://bad%09host"] {
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: [
|
|
MiniMaxSettingsReader.hostKey: host,
|
|
]) == nil)
|
|
#expect(MiniMaxSettingsReader.remainsURL(environment: [
|
|
MiniMaxSettingsReader.remainsURLKey: "\(host)/remains",
|
|
]) == nil)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `endpoint overrides require https and no userinfo`() {
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: [
|
|
MiniMaxSettingsReader.hostKey: "http://platform.minimax.io",
|
|
]) == nil)
|
|
#expect(MiniMaxSettingsReader.remainsURL(environment: [
|
|
MiniMaxSettingsReader.remainsURLKey: "https://user:pass@platform.minimax.io/remains",
|
|
]) == nil)
|
|
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: [
|
|
MiniMaxSettingsReader.hostKey: ":443",
|
|
]) == MiniMaxSettingsReader.hostKey)
|
|
}
|
|
|
|
@Test
|
|
func `endpoint overrides allow MiniMax and custom https hosts`() {
|
|
let env = [
|
|
MiniMaxSettingsReader.hostKey: "platform.minimaxi.com",
|
|
MiniMaxSettingsReader.remainsURLKey: "https://platform.minimax.io/custom/remains",
|
|
]
|
|
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == "platform.minimaxi.com")
|
|
#expect(MiniMaxSettingsReader.remainsURL(environment: env)?.host == "platform.minimax.io")
|
|
|
|
let customEnv = [
|
|
MiniMaxSettingsReader.hostKey: "proxy.example.test",
|
|
MiniMaxSettingsReader.remainsURLKey: "https://proxy.example.test/custom/remains",
|
|
]
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: customEnv) == "proxy.example.test")
|
|
#expect(MiniMaxSettingsReader.remainsURL(environment: customEnv)?.host == "proxy.example.test")
|
|
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: customEnv) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `host endpoint overrides preserve explicit port`() {
|
|
let env = [MiniMaxSettingsReader.hostKey: "proxy.example.test:8443"]
|
|
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == "proxy.example.test:8443")
|
|
#expect(
|
|
MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env).absoluteString ==
|
|
"https://proxy.example.test:8443/user-center/payment/coding-plan?cycle_type=3")
|
|
#expect(
|
|
MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: env).absoluteString ==
|
|
"https://proxy.example.test:8443/v1/api/openplatform/coding_plan/remains")
|
|
}
|
|
|
|
@Test
|
|
func `subscription metadata accepts host names beginning with http`() throws {
|
|
let url = try MiniMaxSubscriptionMetadataFetcher.resolveComboURL(
|
|
region: .global,
|
|
environment: [MiniMaxSettingsReader.hostKey: "https://http-proxy.example.test"])
|
|
|
|
#expect(url.host == "http-proxy.example.test")
|
|
#expect(url.scheme == "https")
|
|
}
|
|
|
|
@Test
|
|
func `scheme less endpoint preserves colon in path`() {
|
|
let env = [MiniMaxSettingsReader.remainsURLKey: "proxy.example.test/api:v1"]
|
|
|
|
#expect(
|
|
MiniMaxSettingsReader.remainsURL(environment: env)?.absoluteString ==
|
|
"https://proxy.example.test/api:v1")
|
|
}
|
|
|
|
@Test
|
|
func `custom https endpoints allow bracketed IPv6 literals`() {
|
|
let env = [
|
|
MiniMaxSettingsReader.hostKey: "[::1]:8443",
|
|
MiniMaxSettingsReader.remainsURLKey: "https://[::1]:8443/remains",
|
|
]
|
|
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == "[::1]:8443")
|
|
#expect(MiniMaxSettingsReader.remainsURL(environment: env)?.host == "::1")
|
|
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: env) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `strict provider endpoint mode rejects custom hosts`() {
|
|
let env = [
|
|
MiniMaxSettingsReader.requireProviderEndpointOverridesKey: "true",
|
|
MiniMaxSettingsReader.hostKey: "proxy.example.test",
|
|
MiniMaxSettingsReader.remainsURLKey: "https://proxy.example.test/custom/remains",
|
|
]
|
|
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil)
|
|
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: env) == MiniMaxSettingsReader.hostKey)
|
|
}
|
|
|
|
@Test
|
|
func `custom https compatibility mode still rejects http and userinfo`() {
|
|
#expect(MiniMaxSettingsReader.hostOverride(environment: [
|
|
MiniMaxSettingsReader.hostKey: "http://proxy.example.test",
|
|
]) == nil)
|
|
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: [
|
|
MiniMaxSettingsReader.remainsURLKey: "https://user:pass@proxy.example.test/remains",
|
|
]) == MiniMaxSettingsReader.remainsURLKey)
|
|
}
|
|
|
|
@Test
|
|
func `explicit endpoint override rejects invalid scheme before network`() async {
|
|
await #expect(throws: ProviderEndpointOverrideError.minimax(MiniMaxSettingsReader.codingPlanURLKey)) {
|
|
_ = try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: "session=abc123",
|
|
environment: [MiniMaxSettingsReader.codingPlanURLKey: "http://platform.minimax.io/coding-plan"],
|
|
includeBillingHistory: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MiniMaxProviderStrategyTests {
|
|
private struct StubClaudeFetcher: ClaudeUsageFetching {
|
|
func loadLatestUsage(model _: String) async throws -> ClaudeUsageSnapshot {
|
|
throw ClaudeUsageError.parseFailed("stub")
|
|
}
|
|
|
|
func debugRawProbe(model _: String) async -> String {
|
|
"stub"
|
|
}
|
|
|
|
func detectVersion() -> String? {
|
|
nil
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `browser cookie import is user initiated app only`() {
|
|
let appContext = self.makeContext(runtime: .app)
|
|
let cliContext = self.makeContext(runtime: .cli)
|
|
|
|
#expect(MiniMaxCodingPlanFetchStrategy.allowsBrowserCookieImport(context: appContext) == false)
|
|
#expect(MiniMaxCodingPlanFetchStrategy.allowsBrowserCookieImport(context: cliContext) == false)
|
|
|
|
ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
#expect(MiniMaxCodingPlanFetchStrategy.allowsBrowserCookieImport(context: appContext))
|
|
#expect(MiniMaxCodingPlanFetchStrategy.allowsBrowserCookieImport(context: cliContext) == false)
|
|
}
|
|
}
|
|
|
|
private func makeContext(runtime: ProviderRuntime) -> ProviderFetchContext {
|
|
let env: [String: String] = [:]
|
|
return ProviderFetchContext(
|
|
runtime: runtime,
|
|
sourceMode: .web,
|
|
includeCredits: false,
|
|
webTimeout: 1,
|
|
webDebugDumpHTML: false,
|
|
verbose: false,
|
|
env: env,
|
|
settings: nil,
|
|
fetcher: UsageFetcher(environment: env),
|
|
claudeFetcher: StubClaudeFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0))
|
|
}
|
|
}
|
|
|
|
struct MiniMaxCookieHeaderTests {
|
|
@Test
|
|
func `normalizes raw cookie header`() {
|
|
let raw = "foo=bar; session=abc123"
|
|
let normalized = MiniMaxCookieHeader.normalized(from: raw)
|
|
#expect(normalized == "foo=bar; session=abc123")
|
|
}
|
|
|
|
@Test
|
|
func `extracts from cookie header line`() {
|
|
let raw = "Cookie: foo=bar; session=abc123"
|
|
let normalized = MiniMaxCookieHeader.normalized(from: raw)
|
|
#expect(normalized == "foo=bar; session=abc123")
|
|
}
|
|
|
|
@Test
|
|
func `extracts from curl header`() {
|
|
let raw = "curl https://platform.minimax.io -H 'Cookie: foo=bar; session=abc123' -H 'accept: */*'"
|
|
let normalized = MiniMaxCookieHeader.normalized(from: raw)
|
|
#expect(normalized == "foo=bar; session=abc123")
|
|
}
|
|
|
|
@Test
|
|
func `extracts from curl cookie flag`() {
|
|
let raw = "curl https://platform.minimax.io --cookie 'foo=bar; session=abc123'"
|
|
let normalized = MiniMaxCookieHeader.normalized(from: raw)
|
|
#expect(normalized == "foo=bar; session=abc123")
|
|
}
|
|
|
|
@Test
|
|
func `extracts auth and group ID from curl`() {
|
|
let raw = """
|
|
curl 'https://platform.minimax.io/v1/api/openplatform/coding_plan/remains?GroupId=123456' \
|
|
-H 'authorization: Bearer token123' \
|
|
-H 'Cookie: foo=bar; session=abc123'
|
|
"""
|
|
let override = MiniMaxCookieHeader.override(from: raw)
|
|
#expect(override?.cookieHeader == "foo=bar; session=abc123")
|
|
#expect(override?.authorizationToken == "token123")
|
|
#expect(override?.groupID == "123456")
|
|
}
|
|
|
|
@Test
|
|
func `extracts auth from uppercase header`() {
|
|
let raw = """
|
|
curl 'https://platform.minimax.io/v1/api/openplatform/coding_plan/remains?GROUP_ID=98765' \
|
|
-H 'Authorization: Bearer token-abc' \
|
|
-H 'Cookie: foo=bar; session=abc123'
|
|
"""
|
|
let override = MiniMaxCookieHeader.override(from: raw)
|
|
#expect(override?.authorizationToken == "token-abc")
|
|
#expect(override?.groupID == "98765")
|
|
}
|
|
|
|
@Test
|
|
func `extracts group ID from combo curl header and cookie`() {
|
|
let raw = """
|
|
curl 'https://www.minimaxi.com/v1/api/openplatform/charge/combo/cycle_audio_resource_package' \
|
|
-b 'foo=bar; minimax_group_id_v2=2013894056999916075' \
|
|
-H 'x-group-id: 2013894056999916075'
|
|
"""
|
|
let override = MiniMaxCookieHeader.override(from: raw)
|
|
#expect(override?.cookieHeader == "foo=bar; minimax_group_id_v2=2013894056999916075")
|
|
#expect(override?.groupID == "2013894056999916075")
|
|
}
|
|
}
|
|
|
|
struct MiniMaxUsageParserTests {
|
|
@Test
|
|
func `signed out check ignores login copy inside scripts`() {
|
|
let html = """
|
|
<html>
|
|
<head>
|
|
<script id="__NEXT_DATA__" type="application/json">
|
|
{
|
|
"props": {
|
|
"pageProps": {
|
|
"_nextI18Next": {
|
|
"initialI18nStore": {
|
|
"zh": {
|
|
"common": {
|
|
"landing_common_login": "登录",
|
|
"login": "Log in"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body><div id="__next">Coding Plan</div></body>
|
|
</html>
|
|
"""
|
|
|
|
#expect(!MiniMaxUsageFetcher._looksSignedOutForTesting(html: html))
|
|
}
|
|
|
|
@Test
|
|
func `signed out check still detects visible login copy`() {
|
|
let html = """
|
|
<html>
|
|
<head><script>{"landing_common_login":"登录"}</script></head>
|
|
<body><main><a>Log in</a></main></body>
|
|
</html>
|
|
"""
|
|
|
|
#expect(MiniMaxUsageFetcher._looksSignedOutForTesting(html: html))
|
|
}
|
|
|
|
@Test
|
|
func `parses planName from concrete fields in remains response`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
|
|
// 1. plan_name
|
|
let jsonPlanName = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"plan_name": "MiniMax Star",
|
|
"model_remains": [{"model_name": "abab6.5"}]
|
|
}
|
|
"""
|
|
let snapshot1 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonPlanName.utf8), now: now)
|
|
#expect(snapshot1.planName == "MiniMax Star")
|
|
|
|
// 2. current_plan_title
|
|
let jsonCurrentPlan = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"current_plan_title": "Coding Plan Pro",
|
|
"model_remains": [{"model_name": "abab6.5"}]
|
|
}
|
|
"""
|
|
let snapshot2 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonCurrentPlan.utf8), now: now)
|
|
#expect(snapshot2.planName == "Coding Plan Pro")
|
|
|
|
// 3. current_subscribe_title
|
|
let jsonSubscribe = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"current_subscribe_title": "Max",
|
|
"model_remains": [{"model_name": "abab6.5"}]
|
|
}
|
|
"""
|
|
let snapshot3 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonSubscribe.utf8), now: now)
|
|
#expect(snapshot3.planName == "Max")
|
|
|
|
// 4. combo_title
|
|
let jsonCombo = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"combo_title": "Combo Star",
|
|
"model_remains": [{"model_name": "abab6.5"}]
|
|
}
|
|
"""
|
|
let snapshot4 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonCombo.utf8), now: now)
|
|
#expect(snapshot4.planName == "Combo Star")
|
|
|
|
// 5. current_combo_card.title
|
|
let jsonComboCard = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"current_combo_card": { "title": "Card Title" },
|
|
"model_remains": [{"model_name": "abab6.5"}]
|
|
}
|
|
"""
|
|
let snapshot5 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonComboCard.utf8), now: now)
|
|
#expect(snapshot5.planName == "Card Title")
|
|
}
|
|
|
|
@Test
|
|
func `toUsageSnapshot maps planName to loginMethod`() {
|
|
let now = Date()
|
|
let snapshot1 = MiniMaxUsageSnapshot(
|
|
planName: "MiniMax Star",
|
|
availablePrompts: nil,
|
|
currentPrompts: nil,
|
|
remainingPrompts: nil,
|
|
windowMinutes: nil,
|
|
usedPercent: nil,
|
|
resetsAt: nil,
|
|
updatedAt: now)
|
|
let usage1 = snapshot1.toUsageSnapshot()
|
|
#expect(usage1.identity?.loginMethod == "MiniMax Star")
|
|
|
|
let snapshot2 = MiniMaxUsageSnapshot(
|
|
planName: nil,
|
|
availablePrompts: nil,
|
|
currentPrompts: nil,
|
|
remainingPrompts: nil,
|
|
windowMinutes: nil,
|
|
usedPercent: nil,
|
|
resetsAt: nil,
|
|
updatedAt: now)
|
|
let usage2 = snapshot2.toUsageSnapshot()
|
|
#expect(usage2.identity?.loginMethod == nil)
|
|
}
|
|
|
|
@Test
|
|
func `parses coding plan snapshot`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let html = """
|
|
<div>Coding Plan</div>
|
|
<div>Max</div>
|
|
<div>Available usage: 1,000 prompts / 5 hours</div>
|
|
<div>Current Usage</div>
|
|
<div>0% Used</div>
|
|
<div>Resets in 4 min</div>
|
|
"""
|
|
|
|
let snapshot = try MiniMaxUsageParser.parse(html: html, now: now)
|
|
|
|
#expect(snapshot.planName == "Max")
|
|
#expect(snapshot.availablePrompts == 1000)
|
|
#expect(snapshot.windowMinutes == 300)
|
|
#expect(snapshot.usedPercent == 0)
|
|
#expect(snapshot.resetsAt == now.addingTimeInterval(240))
|
|
|
|
let usage = snapshot.toUsageSnapshot()
|
|
#expect(usage.primary?.resetDescription == "1000 prompts / 5 hours")
|
|
}
|
|
|
|
@Test
|
|
func `parses coding plan remains response`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let start = 1_700_000_000_000
|
|
let end = start + 5 * 60 * 60 * 1000
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"current_subscribe_title": "Max",
|
|
"model_remains": [
|
|
{
|
|
"current_interval_total_count": 1000,
|
|
"current_interval_usage_count": 250,
|
|
"start_time": \(start),
|
|
"end_time": \(end),
|
|
"remains_time": 240000
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
|
|
let expectedReset = Date(timeIntervalSince1970: TimeInterval(end) / 1000)
|
|
|
|
#expect(snapshot.planName == "Max")
|
|
#expect(snapshot.availablePrompts == 1000)
|
|
#expect(snapshot.currentPrompts == 750)
|
|
#expect(snapshot.remainingPrompts == 250)
|
|
#expect(snapshot.windowMinutes == 300)
|
|
#expect(snapshot.usedPercent == 75)
|
|
#expect(snapshot.resetsAt == expectedReset)
|
|
}
|
|
|
|
@Test
|
|
func `parses model remains services using used quota semantics`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let start = 1_700_000_000_000
|
|
let end = start + 5 * 60 * 60 * 1000
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"current_subscribe_title": "Max",
|
|
"model_remains": [
|
|
{
|
|
"model_name": "M2.7-highspeed",
|
|
"current_interval_total_count": 1000,
|
|
"current_interval_usage_count": 250,
|
|
"start_time": \(start),
|
|
"end_time": \(end),
|
|
"remains_time": 240000
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
|
|
let service = try #require(snapshot.services?.first)
|
|
|
|
#expect(service.displayName == "Text Generation")
|
|
#expect(service.usage == 750)
|
|
#expect(service.remaining == 250)
|
|
#expect(service.limit == 1000)
|
|
#expect(service.percent == 75)
|
|
#expect(snapshot.toUsageSnapshot().primary?.usedPercent == 75)
|
|
}
|
|
|
|
@Test
|
|
func `text generation includes weekly window when provided`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let start = 1_700_000_000_000
|
|
let end = start + 5 * 60 * 60 * 1000
|
|
let weekStart = start - 2 * 24 * 60 * 60 * 1000
|
|
let weekEnd = weekStart + 7 * 24 * 60 * 60 * 1000
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"model_remains": [
|
|
{
|
|
"model_name": "MiniMax-M1",
|
|
"current_interval_total_count": 1000,
|
|
"current_interval_usage_count": 250,
|
|
"start_time": \(start),
|
|
"end_time": \(end),
|
|
"current_weekly_total_count": 6000,
|
|
"current_weekly_usage_count": 5376,
|
|
"weekly_start_time": \(weekStart),
|
|
"weekly_end_time": \(weekEnd)
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
|
|
let services = try #require(snapshot.services)
|
|
#expect(services.count == 2)
|
|
#expect(services[0].serviceType == "Text Generation")
|
|
#expect(services[0].windowType == "5 hours")
|
|
#expect(services[1].serviceType == "Text Generation")
|
|
#expect(services[1].windowType == "Weekly")
|
|
#expect(services[1].usage == 624)
|
|
#expect(services[1].limit == 6000)
|
|
#expect(services[1].timeRange.contains("/"))
|
|
#expect(services[1].timeRange.contains("UTC+8"))
|
|
#expect(!services[1].timeRange.hasPrefix("10:00-10:00"))
|
|
}
|
|
|
|
@Test
|
|
func `legacy plan hides weekly when weekly total is missing or zero`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let start = 1_700_000_000_000
|
|
let end = start + 5 * 60 * 60 * 1000
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"model_remains": [
|
|
{
|
|
"model_name": "MiniMax-M1",
|
|
"current_interval_total_count": 1000,
|
|
"current_interval_usage_count": 250,
|
|
"start_time": \(start),
|
|
"end_time": \(end),
|
|
"current_weekly_total_count": 0,
|
|
"current_weekly_usage_count": 0
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
|
|
let services = try #require(snapshot.services)
|
|
#expect(services.count == 1)
|
|
#expect(services[0].windowType == "5 hours")
|
|
}
|
|
|
|
@Test
|
|
func `parses multi service payload and utc offset reset`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 8 * 3600))
|
|
let now = try #require(calendar.date(from: DateComponents(
|
|
year: 2026,
|
|
month: 3,
|
|
day: 25,
|
|
hour: 11,
|
|
minute: 0)))
|
|
let expectedReset = try #require(calendar.date(from: DateComponents(
|
|
year: 2026,
|
|
month: 3,
|
|
day: 25,
|
|
hour: 15,
|
|
minute: 0)))
|
|
let json = """
|
|
{
|
|
"data": {
|
|
"services": [
|
|
{
|
|
"service_type": "Text Generation",
|
|
"window_type": "5 hours",
|
|
"time_range": "10:00-15:00(UTC+8)",
|
|
"usage": 2,
|
|
"limit": 10
|
|
},
|
|
{
|
|
"service_type": "Image",
|
|
"window_type": "Today",
|
|
"time_range": "2026/03/25 00:00 - 2026/03/26 00:00",
|
|
"usage": "5",
|
|
"limit": "50",
|
|
"percent": "10"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
"""
|
|
|
|
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
|
|
let services = try #require(snapshot.services)
|
|
|
|
#expect(services.count == 2)
|
|
#expect(services[0].usage == 2)
|
|
#expect(services[0].remaining == 8)
|
|
#expect(services[0].percent == 20)
|
|
#expect(services[0].resetsAt == expectedReset)
|
|
#expect(services[1].usage == 5)
|
|
#expect(services[1].remaining == 45)
|
|
#expect(services[1].percent == 10)
|
|
}
|
|
|
|
@Test
|
|
func `parses coding plan remains from data wrapper`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_100)
|
|
let start = 1_700_000_000_000
|
|
let end = start + 5 * 60 * 60 * 1000
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": "0" },
|
|
"data": {
|
|
"current_subscribe_title": "Max",
|
|
"model_remains": [
|
|
{
|
|
"current_interval_total_count": "15000",
|
|
"current_interval_usage_count": "14989",
|
|
"start_time": \(start),
|
|
"end_time": \(end),
|
|
"remains_time": 8941292
|
|
}
|
|
]
|
|
}
|
|
}
|
|
"""
|
|
|
|
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
|
|
let expectedUsed = Double(11) / Double(15000) * 100
|
|
let expectedReset = Date(timeIntervalSince1970: TimeInterval(end) / 1000)
|
|
|
|
#expect(snapshot.planName == "Max")
|
|
#expect(snapshot.availablePrompts == 15000)
|
|
#expect(snapshot.currentPrompts == 11)
|
|
#expect(snapshot.remainingPrompts == 14989)
|
|
#expect(snapshot.windowMinutes == 300)
|
|
#expect(abs((snapshot.usedPercent ?? 0) - expectedUsed) < 0.01)
|
|
#expect(snapshot.resetsAt == expectedReset)
|
|
}
|
|
|
|
@Test
|
|
func `parses coding plan from next data`() throws {
|
|
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let start = 1_700_000_000_000
|
|
let end = start + 5 * 60 * 60 * 1000
|
|
let json = """
|
|
{
|
|
"props": {
|
|
"pageProps": {
|
|
"data": {
|
|
"base_resp": { "status_code": 0 },
|
|
"current_subscribe_title": "Max",
|
|
"model_remains": [
|
|
{
|
|
"current_interval_total_count": 1000,
|
|
"current_interval_usage_count": 250,
|
|
"start_time": \(start),
|
|
"end_time": \(end),
|
|
"remains_time": 240000
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let html = """
|
|
<html>
|
|
<script id="__NEXT_DATA__" type="application/json">\(json)</script>
|
|
</html>
|
|
"""
|
|
|
|
let snapshot = try MiniMaxUsageParser.parse(html: html, now: now)
|
|
let expectedReset = Date(timeIntervalSince1970: TimeInterval(end) / 1000)
|
|
|
|
#expect(snapshot.planName == "Max")
|
|
#expect(snapshot.availablePrompts == 1000)
|
|
#expect(snapshot.currentPrompts == 750)
|
|
#expect(snapshot.remainingPrompts == 250)
|
|
#expect(snapshot.windowMinutes == 300)
|
|
#expect(snapshot.usedPercent == 75)
|
|
#expect(snapshot.resetsAt == expectedReset)
|
|
}
|
|
|
|
@Test
|
|
func `parses HTML with used prefix and reset time`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = TimeZone(identifier: "UTC") ?? .current
|
|
let now = try #require(calendar.date(from: DateComponents(year: 2025, month: 1, day: 1, hour: 10, minute: 0)))
|
|
let expectedReset = try #require(calendar.date(from: DateComponents(
|
|
year: 2025,
|
|
month: 1,
|
|
day: 1,
|
|
hour: 23,
|
|
minute: 30)))
|
|
|
|
let html = """
|
|
<div>Coding Plan Pro</div>
|
|
<div>Available usage: 1,500 prompts / 1.5 hours</div>
|
|
<div>Used 75%</div>
|
|
<div>Resets at 23:30 (UTC)</div>
|
|
"""
|
|
|
|
let snapshot = try MiniMaxUsageParser.parse(html: html, now: now)
|
|
|
|
#expect(snapshot.planName == "Pro")
|
|
#expect(snapshot.availablePrompts == 1500)
|
|
#expect(snapshot.windowMinutes == 90)
|
|
#expect(snapshot.usedPercent == 75)
|
|
#expect(snapshot.resetsAt == expectedReset)
|
|
}
|
|
|
|
@Test
|
|
func `throws on missing cookie response`() {
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": 1004, "status_msg": "cookie is missing, log in again" }
|
|
}
|
|
"""
|
|
|
|
#expect(throws: MiniMaxUsageError.invalidCredentials) {
|
|
try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `throws on string status code when logged out`() {
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": "1004", "status_msg": "login required" }
|
|
}
|
|
"""
|
|
|
|
#expect(throws: MiniMaxUsageError.invalidCredentials) {
|
|
try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `throws on error in data wrapper`() {
|
|
let json = """
|
|
{
|
|
"data": {
|
|
"base_resp": { "status_code": 1004, "status_msg": "unauthorized" }
|
|
}
|
|
}
|
|
"""
|
|
|
|
#expect(throws: MiniMaxUsageError.invalidCredentials) {
|
|
try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `billing history aggregates records locally`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0))
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"consume_token_sum": 999999,
|
|
"total_cnt": 4,
|
|
"charge_records": [
|
|
{
|
|
"consume_token": 1000,
|
|
"consume_cash_after_voucher": 1.25,
|
|
"ymd": "2026-05-17",
|
|
"method": "chat",
|
|
"model": "MiniMax-M1"
|
|
},
|
|
{
|
|
"consume_token": "2000",
|
|
"consume_cash": "2.50",
|
|
"ymd": "2026-05-16",
|
|
"method": "chat",
|
|
"model": "MiniMax-M2"
|
|
},
|
|
{
|
|
"consume_input_token": 1200,
|
|
"consume_output_token": 1800,
|
|
"ymd": "2026-04-18",
|
|
"method": "audio",
|
|
"model": "speech-2.8"
|
|
},
|
|
{
|
|
"consume_token": 4000,
|
|
"ymd": "2026-04-17",
|
|
"method": "old",
|
|
"model": "ignored"
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
let summary = try MiniMaxBillingHistoryParser.parse(
|
|
data: Data(json.utf8),
|
|
now: now,
|
|
calendar: calendar)
|
|
|
|
#expect(summary.todayTokens == 1000)
|
|
#expect(summary.last30DaysTokens == 6000)
|
|
#expect(summary.todayCash == 1.25)
|
|
#expect(summary.last30DaysCash == 3.75)
|
|
#expect(summary.daily.map(\.day) == ["2026-04-18", "2026-05-16", "2026-05-17"])
|
|
#expect(summary.topMethods.first?.name == "audio")
|
|
#expect(summary.topModels.first?.name == "speech-2.8")
|
|
}
|
|
|
|
@Test
|
|
func `billing history preserves date only days in local calendar`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(secondsFromGMT: -7 * 60 * 60))
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"total_cnt": 1,
|
|
"charge_records": [
|
|
{
|
|
"consume_token": 1234,
|
|
"ymd": "2026-05-17",
|
|
"method": "chat",
|
|
"model": "MiniMax-M1"
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
let summary = try MiniMaxBillingHistoryParser.parse(
|
|
data: Data(json.utf8),
|
|
now: now,
|
|
calendar: calendar)
|
|
|
|
#expect(summary.todayTokens == 1234)
|
|
#expect(summary.daily.map(\.day) == ["2026-05-17"])
|
|
}
|
|
|
|
@Test
|
|
func `billing history filters failed records`() throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0))
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let json = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"total_cnt": 5,
|
|
"charge_records": [
|
|
{
|
|
"consume_token": 1000,
|
|
"ymd": "2026-05-17",
|
|
"method": "chat",
|
|
"model": "MiniMax-M1",
|
|
"result": "SUCCESS"
|
|
},
|
|
{
|
|
"consume_token": 2000,
|
|
"ymd": "2026-05-17",
|
|
"method": "chat",
|
|
"model": "MiniMax-M1",
|
|
"result": "FAILED"
|
|
},
|
|
{
|
|
"consume_token": 3000,
|
|
"ymd": "2026-05-17",
|
|
"method": "chat",
|
|
"model": "MiniMax-M1",
|
|
"status": "fail"
|
|
},
|
|
{
|
|
"consume_token": 4000,
|
|
"ymd": "2026-05-17",
|
|
"method": "audio",
|
|
"model": "speech-2.8"
|
|
},
|
|
{
|
|
"consume_token": 5000,
|
|
"ymd": "2026-05-17",
|
|
"method": "video",
|
|
"model": "video-1",
|
|
"status": 0
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
let summary = try MiniMaxBillingHistoryParser.parse(
|
|
data: Data(json.utf8),
|
|
now: now,
|
|
calendar: calendar)
|
|
|
|
// Only SUCCESS (1000) and missing/empty result status (4000) should be included.
|
|
// FAILED (2000), status "fail" (3000), and numeric status 0 (5000) should be skipped.
|
|
#expect(summary.todayTokens == 5000)
|
|
#expect(summary.last30DaysTokens == 5000)
|
|
#expect(summary.daily.map(\.day) == ["2026-05-17"])
|
|
|
|
// Top methods should aggregate only SUCCESS/missing records.
|
|
#expect(summary.topMethods.count == 2)
|
|
#expect(summary.topMethods[0].name == "audio")
|
|
#expect(summary.topMethods[0].tokens == 4000)
|
|
#expect(summary.topMethods[1].name == "chat")
|
|
#expect(summary.topMethods[1].tokens == 1000)
|
|
}
|
|
|
|
@Test
|
|
func `web usage fetch attaches billing history when available`() async throws {
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
let url = try #require(request.url)
|
|
if url.path.contains("coding-plan") {
|
|
return Self.httpResponse(
|
|
url: url,
|
|
body: Self.codingPlanJSON,
|
|
contentType: "application/json")
|
|
}
|
|
#expect(url.path == "/account/amount")
|
|
#expect(url.query?.contains("aggregate=false") == true)
|
|
#expect(request.value(forHTTPHeaderField: "Cookie") == "HERTZ-SESSION=abc")
|
|
let body = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"total_cnt": 1,
|
|
"charge_records": [
|
|
{
|
|
"consume_token": 1234,
|
|
"ymd": "2026-05-17",
|
|
"method": "chat",
|
|
"model": "MiniMax-M1"
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
return Self.httpResponse(url: url, body: body, contentType: "application/json")
|
|
}
|
|
|
|
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: "HERTZ-SESSION=abc",
|
|
region: .global,
|
|
environment: [:],
|
|
session: transport,
|
|
now: now)
|
|
|
|
#expect(snapshot.currentPrompts == 2)
|
|
#expect(snapshot.billingSummary?.todayTokens == 1234)
|
|
#expect(snapshot.billingSummary?.last30DaysTokens == 1234)
|
|
}
|
|
|
|
@Test
|
|
func `web usage fetch keeps paginating billing history until 30 day cutoff`() async throws {
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
let url = try #require(request.url)
|
|
if url.path.contains("coding-plan") {
|
|
return Self.httpResponse(
|
|
url: url,
|
|
body: Self.codingPlanJSON,
|
|
contentType: "application/json")
|
|
}
|
|
let page = URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
|
.queryItems?
|
|
.first { $0.name == "page" }?
|
|
.value ?? "1"
|
|
let recordDay = page == "3" ? "2026-04-17" : "2026-05-17"
|
|
let records = (0..<100)
|
|
.map { _ in
|
|
"""
|
|
{"consume_token":1,"ymd":"\(recordDay)","method":"chat","model":"MiniMax-M1"}
|
|
"""
|
|
}
|
|
.joined(separator: ",")
|
|
let body = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"total_cnt": 250,
|
|
"charge_records": [\(records)]
|
|
}
|
|
"""
|
|
return Self.httpResponse(url: url, body: body, contentType: "application/json")
|
|
}
|
|
|
|
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: "HERTZ-SESSION=abc",
|
|
region: .global,
|
|
environment: [:],
|
|
session: transport,
|
|
now: now)
|
|
|
|
let billingRequests = await transport.requests().filter { $0.url?.path == "/account/amount" }
|
|
#expect(billingRequests.count == 3)
|
|
#expect(snapshot.billingSummary?.last30DaysTokens == 200)
|
|
}
|
|
|
|
@Test
|
|
func `web usage fetch skips billing history when optional usage is disabled`() async throws {
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
let url = try #require(request.url)
|
|
#expect(url.path.contains("coding-plan"))
|
|
return Self.httpResponse(
|
|
url: url,
|
|
body: Self.codingPlanJSON,
|
|
contentType: "application/json")
|
|
}
|
|
|
|
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: "HERTZ-SESSION=abc",
|
|
region: .global,
|
|
environment: [:],
|
|
includeBillingHistory: false,
|
|
session: transport,
|
|
now: now)
|
|
|
|
let requests = await transport.requests()
|
|
#expect(snapshot.currentPrompts == 2)
|
|
#expect(snapshot.billingSummary == nil)
|
|
#expect(requests.count == 1)
|
|
}
|
|
|
|
@Test
|
|
func `web usage fetch keeps quota when billing history is forbidden`() async throws {
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
let url = try #require(request.url)
|
|
if url.path.contains("coding-plan") {
|
|
return Self.httpResponse(
|
|
url: url,
|
|
body: Self.codingPlanJSON,
|
|
contentType: "application/json")
|
|
}
|
|
return Self.httpResponse(url: url, body: "{}", statusCode: 403, contentType: "application/json")
|
|
}
|
|
|
|
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: "HERTZ-SESSION=abc",
|
|
region: .global,
|
|
environment: [:],
|
|
session: transport,
|
|
now: now)
|
|
|
|
#expect(snapshot.currentPrompts == 2)
|
|
#expect(snapshot.billingSummary == nil)
|
|
}
|
|
|
|
@Test
|
|
func `web usage fetch preserves stale bearer failure during billing history`() async throws {
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
let url = try #require(request.url)
|
|
if url.path.contains("coding-plan") {
|
|
return Self.httpResponse(
|
|
url: url,
|
|
body: Self.codingPlanJSON,
|
|
contentType: "application/json")
|
|
}
|
|
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer stale")
|
|
return Self.httpResponse(url: url, body: "{}", statusCode: 403, contentType: "application/json")
|
|
}
|
|
|
|
await #expect(throws: MiniMaxUsageError.invalidCredentials) {
|
|
try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: "HERTZ-SESSION=abc",
|
|
authorizationToken: "stale",
|
|
region: .global,
|
|
environment: [:],
|
|
session: transport,
|
|
now: now)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `web usage fetch preserves billing history cancellation`() async throws {
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
let url = try #require(request.url)
|
|
if url.path.contains("coding-plan") {
|
|
return Self.httpResponse(
|
|
url: url,
|
|
body: Self.codingPlanJSON,
|
|
contentType: "application/json")
|
|
}
|
|
throw CancellationError()
|
|
}
|
|
|
|
await #expect(throws: CancellationError.self) {
|
|
try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: "HERTZ-SESSION=abc",
|
|
region: .global,
|
|
environment: [:],
|
|
session: transport,
|
|
now: now)
|
|
}
|
|
}
|
|
|
|
private static let codingPlanJSON = """
|
|
{
|
|
"base_resp": { "status_code": 0 },
|
|
"data": {
|
|
"plan_name": "Max",
|
|
"model_remains": [
|
|
{
|
|
"model_name": "MiniMax-M1",
|
|
"current_interval_total_count": 10,
|
|
"current_interval_usage_count": 8,
|
|
"start_time": 1779019200,
|
|
"end_time": 1779037200,
|
|
"remains_time": 3600
|
|
}
|
|
]
|
|
}
|
|
}
|
|
"""
|
|
|
|
private static func httpResponse(
|
|
url: URL,
|
|
body: String,
|
|
statusCode: Int = 200,
|
|
contentType: String) -> (Data, URLResponse)
|
|
{
|
|
let response = HTTPURLResponse(
|
|
url: url,
|
|
statusCode: statusCode,
|
|
httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": contentType])!
|
|
return (Data(body.utf8), response)
|
|
}
|
|
}
|
|
|
|
struct MiniMaxAPIRegionTests {
|
|
@Test
|
|
func `defaults to global hosts`() {
|
|
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: [:])
|
|
let remains = MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: [:])
|
|
#expect(codingPlan.host == "platform.minimax.io")
|
|
#expect(remains.host == "platform.minimax.io")
|
|
}
|
|
|
|
@Test
|
|
func `uses china mainland hosts`() {
|
|
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .chinaMainland, environment: [:])
|
|
let remains = MiniMaxUsageFetcher.resolveRemainsURL(region: .chinaMainland, environment: [:])
|
|
#expect(codingPlan.host == "platform.minimaxi.com")
|
|
#expect(remains.host == "platform.minimaxi.com")
|
|
#expect(codingPlan.query == "cycle_type=3")
|
|
}
|
|
|
|
@Test
|
|
func `resolves web remains fallback hosts`() {
|
|
let global = MiniMaxUsageFetcher.resolveRemainsURLs(region: .global, environment: [:])
|
|
let china = MiniMaxUsageFetcher.resolveRemainsURLs(region: .chinaMainland, environment: [:])
|
|
|
|
#expect(global.map(\.host).contains("platform.minimax.io"))
|
|
#expect(global.map(\.host).contains("www.minimax.io"))
|
|
#expect(china.map(\.host).contains("platform.minimaxi.com"))
|
|
#expect(china.map(\.host).contains("www.minimaxi.com"))
|
|
}
|
|
|
|
@Test
|
|
func `resolves official token plan remains URL`() {
|
|
let url = MiniMaxUsageFetcher.resolveTokenPlanRemainsURL(region: .chinaMainland)
|
|
#expect(url.host == "api.minimaxi.com")
|
|
#expect(url.path == "/v1/token_plan/remains")
|
|
}
|
|
|
|
@Test
|
|
func `host override wins for remains and coding plan`() {
|
|
let env = [MiniMaxSettingsReader.hostKey: "api.minimaxi.com"]
|
|
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env)
|
|
let remains = MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: env)
|
|
#expect(codingPlan.host == "api.minimaxi.com")
|
|
#expect(remains.host == "api.minimaxi.com")
|
|
}
|
|
|
|
@Test
|
|
func `billing history url uses account amount endpoint`() {
|
|
let url = MiniMaxUsageFetcher.resolveBillingHistoryURL(region: .chinaMainland, environment: [:], page: 2)
|
|
#expect(url.host == "platform.minimaxi.com")
|
|
#expect(url.path == "/account/amount")
|
|
#expect(url.query?.contains("page=2") == true)
|
|
#expect(url.query?.contains("limit=100") == true)
|
|
#expect(url.query?.contains("aggregate=false") == true)
|
|
}
|
|
|
|
@Test
|
|
func `remains url override beats host`() {
|
|
let env = [MiniMaxSettingsReader.remainsURLKey: "https://platform.minimaxi.com/custom/remains"]
|
|
let remains = MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: env)
|
|
#expect(remains.absoluteString == "https://platform.minimaxi.com/custom/remains")
|
|
}
|
|
|
|
@Test
|
|
func `origin uses coding plan override host`() {
|
|
let env = [MiniMaxSettingsReader.codingPlanURLKey: "https://api.minimaxi.com/custom/path?cycle_type=3"]
|
|
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env)
|
|
let origin = MiniMaxUsageFetcher.originURL(from: codingPlan)
|
|
#expect(origin.absoluteString == "https://api.minimaxi.com")
|
|
}
|
|
|
|
@Test
|
|
func `origin strips host override path`() {
|
|
let env = [MiniMaxSettingsReader.hostKey: "https://api.minimaxi.com/custom/path"]
|
|
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env)
|
|
let origin = MiniMaxUsageFetcher.originURL(from: codingPlan)
|
|
#expect(origin.absoluteString == "https://api.minimaxi.com")
|
|
}
|
|
}
|