Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum AlibabaTokenPlanUsageError: LocalizedError, Sendable, Equatable {
public struct AlibabaTokenPlanUsageFetcher: Sendable {
private struct PersonalAPIContext: Sendable {
let apiCookieHeader: String
let secToken: String?
let region: AlibabaTokenPlanAPIRegion
let environment: [String: String]
let session: URLSession
Expand Down Expand Up @@ -130,12 +131,20 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
}

if region.usesPersonalTokenPlanAPI {
return try await self.fetchPersonalUsage(
let secToken = await self.resolveSECToken(
Comment thread
wdmitchelluk marked this conversation as resolved.
Outdated
dashboardCookieHeader: normalizedDashboardHeader,
apiCookieHeader: normalizedAPIHeader,
region: region,
environment: environment,
now: now,
session: apiSession)
session: dashboardSession)
return try await self.fetchPersonalUsage(
context: PersonalAPIContext(
apiCookieHeader: normalizedAPIHeader,
secToken: secToken,
region: region,
environment: environment,
session: apiSession),
now: now)
}

let secToken = await self.resolveSECToken(
Expand Down Expand Up @@ -311,25 +320,19 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
}

private static func fetchPersonalUsage(
apiCookieHeader: String,
region: AlibabaTokenPlanAPIRegion,
environment: [String: String],
now: Date,
session: URLSession) async throws -> AlibabaTokenPlanUsageSnapshot
context: PersonalAPIContext,
now: Date) async throws -> AlibabaTokenPlanUsageSnapshot
{
let context = PersonalAPIContext(
apiCookieHeader: apiCookieHeader,
region: region,
environment: environment,
session: session)
self.log.info(
"Fetching Alibaba Token Plan Personal usage",
metadata: [
"apiHost": self.resolveQuotaURL(region: region, environment: environment).host ?? "unknown",
"region": region.rawValue,
"apiCookieNames": self.cookieNamesDescription(from: apiCookieHeader),
"hasCSRF": self.hasCSRF(in: apiCookieHeader) ? "1" : "0",
"secTokenSource": "not-required",
"apiHost": self.resolveQuotaURL(
region: context.region,
environment: context.environment).host ?? "unknown",
"region": context.region.rawValue,
"apiCookieNames": self.cookieNamesDescription(from: context.apiCookieHeader),
"hasCSRF": self.hasCSRF(in: context.apiCookieHeader) ? "1" : "0",
"secTokenSource": context.secToken == nil ? "missing" : "resolved",
])

let usageData = try await self.fetchPersonalAPI(
Expand All @@ -338,7 +341,7 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
context: context)
let subscriptionData = await self.fetchOptionalPersonalAPI(
api: self.personalSubscriptionAPI,
dataParameters: ["commodityCode": region.tokenPlanProductCode],
dataParameters: ["commodityCode": context.region.tokenPlanProductCode],
context: context)
let quotaConfigData = await self.fetchOptionalPersonalAPI(
api: self.personalQuotaConfigAPI,
Expand Down Expand Up @@ -382,9 +385,7 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
request.httpBody = try self.personalAPIRequestBody(
api: api,
dataParameters: dataParameters,
apiCookieHeader: context.apiCookieHeader,
region: context.region,
environment: context.environment)
context: context)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept")
request.setValue(context.apiCookieHeader, forHTTPHeaderField: "Cookie")
Expand Down Expand Up @@ -445,11 +446,9 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
private static func personalAPIRequestBody(
api: String,
dataParameters: [String: String],
apiCookieHeader: String,
region: AlibabaTokenPlanAPIRegion,
environment: [String: String]) throws -> Data
context: PersonalAPIContext) throws -> Data
{
let dashboardURL = self.dashboardURL(region: region, environment: environment)
let dashboardURL = self.dashboardURL(region: context.region, environment: context.environment)
var cornerstone: [String: Any] = [
"feTraceId": UUID().uuidString.lowercased(),
"feURL": dashboardURL.absoluteString,
Expand All @@ -459,12 +458,12 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
"switchAgent": 1_233_135,
"switchUserType": 3,
"domain": dashboardURL.host ?? "",
"consoleSite": region.personalConsoleSite,
"consoleSite": context.region.personalConsoleSite,
"userNickName": "",
"userPrincipalName": "",
"xsp_lang": "en-US",
]
if let anonymousID = self.extractCookieValue(name: "cna", from: apiCookieHeader), !anonymousID.isEmpty {
if let anonymousID = self.extractCookieValue(name: "cna", from: context.apiCookieHeader), !anonymousID.isEmpty {
cornerstone["X-Anonymous-Id"] = anonymousID
}
var apiData = dataParameters as [String: Any]
Expand All @@ -480,13 +479,17 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
}

var body = URLComponents()
body.queryItems = [
var queryItems = [
URLQueryItem(name: "product", value: self.personalConsoleProduct),
URLQueryItem(name: "action", value: region.personalAPIAction),
URLQueryItem(name: "region", value: region.currentRegionID),
URLQueryItem(name: "action", value: context.region.personalAPIAction),
URLQueryItem(name: "region", value: context.region.currentRegionID),
URLQueryItem(name: "language", value: "en-US"),
URLQueryItem(name: "params", value: paramsJSON),
]
if let secToken = context.secToken, !secToken.isEmpty {
queryItems.append(URLQueryItem(name: "sec_token", value: secToken))
}
body.queryItems = queryItems
return Data((body.percentEncodedQuery ?? "").utf8)
}

Expand Down Expand Up @@ -714,11 +717,11 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
throw AlibabaTokenPlanUsageError.apiError(message)
}

if self.findBoolValues(forKeys: ["Success", "success"], in: dictionary).contains(false) {
let code = self.findFirstString(forKeys: ["errorCode", "Code", "code"], in: dictionary)
if let failedPayload = self.findFailedPayload(in: dictionary) {
let code = self.findFirstString(forKeys: ["errorCode", "Code", "code"], in: failedPayload)
let message = self.findFirstString(
forKeys: ["errorMsg", "Message", "message", "msg", "Code", "code"],
in: dictionary) ?? "request was not successful"
in: failedPayload) ?? "request was not successful"
if self.isLoginOrTokenError(code: code, message: message) {
throw AlibabaTokenPlanUsageError.loginRequired
}
Expand Down Expand Up @@ -979,16 +982,25 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable {
return nil
}

private static func findBoolValues(forKeys keys: [String], in value: Any) -> [Bool] {
if let dict = value as? [String: Any] {
let directValues = keys.compactMap { self.parseBool(dict[$0]) }
let nestedValues = dict.values.flatMap { self.findBoolValues(forKeys: keys, in: $0) }
return directValues + nestedValues
private static func findFailedPayload(in value: Any) -> [String: Any]? {
if let dictionary = value as? [String: Any] {
if self.parseBool(dictionary["Success"]) == false || self.parseBool(dictionary["success"]) == false {
return dictionary
}
for nestedValue in dictionary.values {
if let failedPayload = self.findFailedPayload(in: nestedValue) {
return failedPayload
}
}
}
if let array = value as? [Any] {
return array.flatMap { self.findBoolValues(forKeys: keys, in: $0) }
for item in array {
if let failedPayload = self.findFailedPayload(in: item) {
return failedPayload
}
}
}
return []
return nil
}

private static func findFirstInt(forKeys keys: [String], in value: Any) -> Int? {
Expand Down
155 changes: 120 additions & 35 deletions Tests/CodexBarTests/AlibabaTokenPlanProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,31 @@ struct AlibabaTokenPlanUsageParsingTests {
}
}

@Test
func `Personal nested gateway error does not report outer success code`() {
let json = """
{
"code": "200",
"data": {
"success": false,
"httpStatus": 200,
"errorCode": "BailianGateway.Workspace.NotAuthorised",
"errorMsg": "BailianGateway.Workspace.NotAuthorised"
},
"httpStatusCode": "200",
"successResponse": true
}
"""

#expect(throws: AlibabaTokenPlanUsageError.apiError("BailianGateway.Workspace.NotAuthorised")) {
try AlibabaTokenPlanPersonalUsageParser.parse(
from: Data(json.utf8),
subscriptionData: nil,
quotaConfigData: nil,
now: Date(timeIntervalSince1970: 1_700_000_000))
}
}

@Test
func `parses subscription summary payload`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
Expand Down Expand Up @@ -546,7 +571,7 @@ struct AlibabaTokenPlanUsageParsingTests {
}

@Test
func `mainland Personal fetch uses quota host cookies without requiring SEC token`() async throws {
func `Personal fetch includes resolved SEC token`() async throws {
defer {
AlibabaTokenPlanStubURLProtocol.handler = nil
}
Expand All @@ -556,46 +581,105 @@ struct AlibabaTokenPlanUsageParsingTests {
let quotaBody = try #require(
String(data: alibabaTokenPlanFixture("personal_quota_config"), encoding: .utf8))

AlibabaTokenPlanStubURLProtocol.handler = { request in
guard let url = request.url else { throw URLError(.badURL) }
#expect(url.host == "bailian-cs.console.aliyun.com")
#expect(request.httpMethod == "POST")
#expect(request.value(forHTTPHeaderField: "Cookie") == "quota_only=quota")
#expect(request.value(forHTTPHeaderField: "Origin") == "https://bailian.console.aliyun.com")
let body = Self.requestBodyString(from: request)
#expect(!body.contains("sec_token"))
#expect(body.removingPercentEncoding?.contains("cornerstoneParam") == true)

let api = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.first(where: { $0.name == "api" })?
.value
switch api {
case "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage":
return Self.makeResponse(url: url, body: usageBody, statusCode: 200)
case "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription":
#expect(body.removingPercentEncoding?.contains("sfm_tokenplansolo_public_cn") == true)
return Self.makeResponse(url: url, body: subscriptionBody, statusCode: 200)
case "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/quota-config":
return Self.makeResponse(url: url, body: quotaBody, statusCode: 200)
default:
throw URLError(.unsupportedURL)
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [AlibabaTokenPlanStubURLProtocol.self]
let session = URLSession(configuration: configuration)
for region in [AlibabaTokenPlanAPIRegion.internationalPersonal, .chinaMainlandPersonal] {
AlibabaTokenPlanStubURLProtocol.handler = { request in
guard let url = request.url else { throw URLError(.badURL) }
if url.host == region.dashboardURL.host, request.httpMethod == "GET" {
#expect(request.value(forHTTPHeaderField: "Cookie") == "dashboard_only=dashboard")
return Self.makeResponse(
url: url,
body: "<script>sec_token = \"personal-token\";</script>",
statusCode: 200)
}
#expect(url.host == URL(string: region.quotaBaseURLString)?.host)
#expect(request.httpMethod == "POST")
#expect(request.value(forHTTPHeaderField: "Cookie") == "quota_only=quota")
#expect(request.value(forHTTPHeaderField: "Origin") == region.dashboardOriginURLString)
let body = Self.requestBodyString(from: request)
#expect(body.contains("sec_token=personal-token"))
#expect(body.removingPercentEncoding?.contains("cornerstoneParam") == true)

let api = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.first(where: { $0.name == "api" })?
.value
switch api {
case "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage":
return Self.makeResponse(url: url, body: usageBody, statusCode: 200)
case "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription":
#expect(body.removingPercentEncoding?.contains(region.tokenPlanProductCode) == true)
return Self.makeResponse(url: url, body: subscriptionBody, statusCode: 200)
case "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/quota-config":
return Self.makeResponse(url: url, body: quotaBody, statusCode: 200)
default:
throw URLError(.unsupportedURL)
}
}

let snapshot = try await AlibabaTokenPlanUsageFetcher.fetchUsage(
apiCookieHeader: "quota_only=quota",
dashboardCookieHeader: "dashboard_only=dashboard",
region: region,
environment: [:],
session: session)

#expect(snapshot.planName == "Pro")
#expect(snapshot.toUsageSnapshot().primary != nil)
#expect(snapshot.toUsageSnapshot().secondary != nil)
}
}

@Test
func `Team fetch retains subscription summary routes`() async throws {
defer {
AlibabaTokenPlanStubURLProtocol.handler = nil
}
let body = """
{
"Success": true,
"Data": {
"TotalCount": 1,
"TotalValue": 1000,
"TotalSurplusValue": 900
}
}
"""
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [AlibabaTokenPlanStubURLProtocol.self]
let session = URLSession(configuration: configuration)
let snapshot = try await AlibabaTokenPlanUsageFetcher.fetchUsage(
apiCookieHeader: "quota_only=quota",
dashboardCookieHeader: "dashboard_only=dashboard",
region: .chinaMainlandPersonal,
environment: [:],
session: session)

#expect(snapshot.planName == "Pro")
#expect(snapshot.toUsageSnapshot().primary != nil)
#expect(snapshot.toUsageSnapshot().secondary != nil)
for region in [AlibabaTokenPlanAPIRegion.international, .chinaMainland] {
AlibabaTokenPlanStubURLProtocol.handler = { request in
guard let url = request.url else { throw URLError(.badURL) }
if url.host == region.dashboardURL.host, request.httpMethod == "GET" {
return Self.makeResponse(
url: url,
body: "<script>sec_token = \"team-token\";</script>",
statusCode: 200)
}
#expect(url.host == region.dashboardURL.host)
#expect(request.httpMethod == "POST")
let requestBody = Self.requestBodyString(from: request)
#expect(requestBody.contains("GetSubscriptionSummary"))
#expect(requestBody.contains(region.tokenPlanProductCode))
#expect(requestBody.contains("sec_token=team-token"))
#expect(!requestBody.contains("tokenplan%2Fpersonal"))
return Self.makeResponse(url: url, body: body, statusCode: 200)
}

let snapshot = try await AlibabaTokenPlanUsageFetcher.fetchUsage(
apiCookieHeader: "api_only=api",
dashboardCookieHeader: "dashboard_only=dashboard",
region: region,
environment: [:],
session: session)

#expect(snapshot.totalQuota == 1000)
#expect(snapshot.remainingQuota == 900)
}
}

@Test
Expand Down Expand Up @@ -1147,7 +1231,8 @@ final class AlibabaTokenPlanStubURLProtocol: URLProtocol {

override static func canInit(with request: URLRequest) -> Bool {
guard let host = request.url?.host else { return false }
return host == "bailian.console.aliyun.com" ||
return host == "modelstudio.console.alibabacloud.com" ||
host == "bailian.console.aliyun.com" ||
host == "bailian-cs.console.aliyun.com" ||
host == "bailian-singapore-cs.alibabacloud.com" ||
host == "alibaba-token-plan.test" ||
Expand Down