import Foundation import Testing @testable import CodexBarCore struct StepFunSettingsReaderTests { @Test func `reads STEPFUN_TOKEN`() { let env = ["STEPFUN_TOKEN": "some-oasis-token-value"] #expect(StepFunSettingsReader.token(environment: env) == "some-oasis-token-value") } @Test func `reads STEPFUN_USERNAME`() { let env = ["STEPFUN_USERNAME": "user@example.com"] #expect(StepFunSettingsReader.username(environment: env) == "user@example.com") } @Test func `reads STEPFUN_PASSWORD`() { let env = ["STEPFUN_PASSWORD": "secret123"] #expect(StepFunSettingsReader.password(environment: env) == "secret123") } @Test func `trims whitespace from token`() { let env = ["STEPFUN_TOKEN": " some-token "] #expect(StepFunSettingsReader.token(environment: env) == "some-token") } @Test func `strips double quotes from token`() { let env = ["STEPFUN_TOKEN": "\"some-token\""] #expect(StepFunSettingsReader.token(environment: env) == "some-token") } @Test func `strips single quotes from token`() { let env = ["STEPFUN_TOKEN": "'some-token'"] #expect(StepFunSettingsReader.token(environment: env) == "some-token") } @Test func `returns nil when no env vars present`() { #expect(StepFunSettingsReader.token(environment: [:]) == nil) #expect(StepFunSettingsReader.username(environment: [:]) == nil) #expect(StepFunSettingsReader.password(environment: [:]) == nil) } @Test func `returns nil for empty values`() { let env = ["STEPFUN_TOKEN": "", "STEPFUN_USERNAME": "", "STEPFUN_PASSWORD": ""] #expect(StepFunSettingsReader.token(environment: env) == nil) #expect(StepFunSettingsReader.username(environment: env) == nil) #expect(StepFunSettingsReader.password(environment: env) == nil) } @Test func `returns nil for whitespace-only values`() { let env = ["STEPFUN_TOKEN": " "] #expect(StepFunSettingsReader.token(environment: env) == nil) } } struct StepFunProviderTokenResolverTests { @Test func `resolves token from environment`() { let env = ["STEPFUN_TOKEN": "my-test-token"] let resolution = ProviderTokenResolver.stepfunResolution(environment: env) #expect(resolution?.token == "my-test-token") #expect(resolution?.source == .environment) } @Test func `returns nil when token absent`() { let resolution = ProviderTokenResolver.stepfunResolution(environment: [:]) #expect(resolution == nil) } } struct StepFunUsageFetcherParsingTests { @Test func `parses real API response format with string timestamps and integer rates`() throws { // This matches the actual StepFun API response format: // - timestamps as strings (e.g. "1777528800") // - rates can be integers (e.g. 1) or floats (e.g. 0.99781543) let json = """ { "status": 1, "desc": "", "five_hour_usage_left_rate": 1, "five_hour_usage_reset_time": "1777528800", "weekly_usage_left_rate": 0.99781543, "weekly_usage_reset_time": "1777899600" } """ let data = Data(json.utf8) let snapshot = try StepFunUsageFetcher._parseSnapshotForTesting(data) #expect(snapshot.fiveHourUsageLeftRate == 1.0) #expect(snapshot.weeklyUsageLeftRate > 0.997 && snapshot.weeklyUsageLeftRate < 0.998) } @Test func `parses response with float rates and integer timestamps`() throws { let json = """ { "status": 1, "five_hour_usage_left_rate": 0.75, "weekly_usage_left_rate": 0.5, "five_hour_usage_reset_time": 1746000000, "weekly_usage_reset_time": 1746500000 } """ let data = Data(json.utf8) let snapshot = try StepFunUsageFetcher._parseSnapshotForTesting(data) #expect(snapshot.fiveHourUsageLeftRate == 0.75) #expect(snapshot.weeklyUsageLeftRate == 0.5) } @Test func `throws on failed API status`() { let json = """ { "status": 0, "message": "Unauthorized", "five_hour_usage_left_rate": 0.75, "weekly_usage_left_rate": 0.5, "five_hour_usage_reset_time": "1746000000", "weekly_usage_reset_time": "1746500000" } """ let data = Data(json.utf8) #expect(throws: StepFunUsageError.self) { try StepFunUsageFetcher._parseSnapshotForTesting(data) } } @Test func `throws on missing fields`() { let json = """ { "status": 1 } """ let data = Data(json.utf8) #expect(throws: StepFunUsageError.self) { try StepFunUsageFetcher._parseSnapshotForTesting(data) } } @Test func `throws on invalid JSON`() { let data = Data("not json".utf8) #expect(throws: StepFunUsageError.self) { try StepFunUsageFetcher._parseSnapshotForTesting(data) } } @Test func `snapshot maps to UsageSnapshot correctly`() throws { let json = """ { "status": 1, "five_hour_usage_left_rate": 0.8, "weekly_usage_left_rate": 0.6, "five_hour_usage_reset_time": "1746000000", "weekly_usage_reset_time": "1746500000" } """ let data = Data(json.utf8) let snapshot = try StepFunUsageFetcher._parseSnapshotForTesting(data) let usage = snapshot.toUsageSnapshot() // Five-hour window: 20% used (1.0 - 0.8) let primaryUsed = usage.primary?.usedPercent ?? 0 #expect(primaryUsed > 19.9 && primaryUsed < 20.1) // Weekly window: 40% used (1.0 - 0.6) let secondaryUsed = usage.secondary?.usedPercent ?? 0 #expect(secondaryUsed > 39.9 && secondaryUsed < 40.1) #expect(usage.secondary?.windowMinutes == 10080) // Identity #expect(usage.identity?.providerID == .stepfun) #expect(usage.identity?.loginMethod == "password") } @Test func `clamps used percent to 0-100 range`() throws { let json = """ { "status": 1, "five_hour_usage_left_rate": 0.0, "weekly_usage_left_rate": 1, "five_hour_usage_reset_time": "1746000000", "weekly_usage_reset_time": "1746500000" } """ let data = Data(json.utf8) let snapshot = try StepFunUsageFetcher._parseSnapshotForTesting(data) let usage = snapshot.toUsageSnapshot() // 0% remaining → 100% used #expect(usage.primary?.usedPercent == 100.0) // 100% remaining → 0% used (integer 1 parsed as 1.0) #expect(usage.secondary?.usedPercent == 0.0) } } struct StepFunTokenNormalizerTests { @Test func `extracts Oasis-Token from cookie header`() { let input = "Oasis-Token=abc123...def456; Oasis-Webid=someid" #expect(StepFunTokenNormalizer.normalize(input) == "abc123...def456") } @Test func `returns raw value when not a cookie header`() { let input = "abc123...def456" #expect(StepFunTokenNormalizer.normalize(input) == "abc123...def456") } @Test func `returns empty for empty string`() { #expect(StepFunTokenNormalizer.normalize("").isEmpty) } @Test func `trims whitespace`() { #expect(StepFunTokenNormalizer.normalize(" token123 ") == "token123") } } @Suite(.serialized) struct StepFunTokenRefreshTests { 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 `refresh token returns combined token pair`() async throws { try await self.withStubProtocol { recorder in StepFunStubURLProtocol.handler = { request in #expect(request.url?.path.contains("RefreshToken") == true) #expect(request.value(forHTTPHeaderField: "Oasis-Token") == "old-access...old-refresh") #expect(request.value(forHTTPHeaderField: "Cookie")?.contains("old-access...old-refresh") == true) recorder.recordRefreshCall() return Self.jsonResponse( for: request, body: """ { "accessToken": {"raw": "new-access"}, "refreshToken": {"raw": "new-refresh"} } """) } let token = try await StepFunUsageFetcher.refreshToken(token: "old-access...old-refresh") #expect(token == "new-access...new-refresh") #expect(recorder.refreshCallCount == 1) } } @Test func `manual token auth failure refreshes token account and retries usage`() async throws { let accountID = UUID() let updateRecorder = StepFunTokenUpdateRecorder() try await self.withStubProtocol { recorder in StepFunStubURLProtocol.handler = { request in let path = request.url?.path ?? "" if path.contains("QueryStepPlanRateLimit") { let call = recorder.recordUsageCall() if call == 1 { #expect(request.value(forHTTPHeaderField: "Cookie")? .contains("old-access...old-refresh") == true) return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"unauthorized"}"#) } #expect(request.value(forHTTPHeaderField: "Cookie")?.contains("new-access...new-refresh") == true) return Self.usageResponse(for: request) } if path.contains("RefreshToken") { recorder.recordRefreshCall() #expect(request.value(forHTTPHeaderField: "Oasis-Token") == "old-access...old-refresh") return Self.jsonResponse( for: request, body: """ { "accessToken": {"raw": "new-access"}, "refreshToken": {"raw": "new-refresh"} } """) } if path.contains("GetStepPlanStatus") { return Self.jsonResponse( for: request, body: #"{"status":1,"subscription":{"name":"Plus","plan_type":1,"status":1}}"#) } return Self.jsonResponse(for: request, statusCode: 404, body: #"{"error":"unexpected"}"#) } let settings = ProviderSettingsSnapshot.make( stepfun: ProviderSettingsSnapshot.StepFunProviderSettings( cookieSource: .manual, manualToken: "old-access...old-refresh")) let context = self.makeContext( settings: settings, selectedTokenAccountID: accountID, tokenUpdater: { provider, updatedAccountID, token in #expect(provider == .stepfun) #expect(updatedAccountID == accountID) await updateRecorder.record(token) }) let result = try await StepFunWebFetchStrategy().fetch(context) #expect(result.usage.identity?.loginMethod == "Plus") #expect(recorder.usageCallCount == 2) #expect(recorder.refreshCallCount == 1) let updatedToken = await updateRecorder.recordedToken() #expect(updatedToken == "new-access...new-refresh") } } @Test func `manual token auth failure refreshes settings token and retries usage`() async throws { let updateRecorder = StepFunTokenUpdateRecorder() try await self.withStubProtocol { recorder in StepFunStubURLProtocol.handler = { request in let path = request.url?.path ?? "" if path.contains("QueryStepPlanRateLimit") { let call = recorder.recordUsageCall() if call == 1 { return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"unauthorized"}"#) } #expect(request.value(forHTTPHeaderField: "Cookie")?.contains("new-access...new-refresh") == true) return Self.usageResponse(for: request) } if path.contains("RefreshToken") { recorder.recordRefreshCall() return Self.jsonResponse( for: request, body: """ { "accessToken": {"raw": "new-access"}, "refreshToken": {"raw": "new-refresh"} } """) } if path.contains("GetStepPlanStatus") { return Self.jsonResponse( for: request, body: #"{"status":1,"subscription":{"name":"Plus","plan_type":1,"status":1}}"#) } return Self.jsonResponse(for: request, statusCode: 404, body: #"{"error":"unexpected"}"#) } let settings = ProviderSettingsSnapshot.make( stepfun: ProviderSettingsSnapshot.StepFunProviderSettings( cookieSource: .manual, manualToken: "old-access...old-refresh")) let context = self.makeContext( settings: settings, manualTokenUpdater: { provider, token in #expect(provider == .stepfun) await updateRecorder.record(token) }) _ = try await StepFunWebFetchStrategy().fetch(context) #expect(recorder.usageCallCount == 2) #expect(recorder.refreshCallCount == 1) let updatedToken = await updateRecorder.recordedToken() #expect(updatedToken == "new-access...new-refresh") } } @Test func `stale cached token falls back to configured env token`() async throws { CookieHeaderCache.store(provider: .stepfun, cookieHeader: "stale-access...stale-refresh", sourceLabel: "test") defer { CookieHeaderCache.clear(provider: .stepfun) } try await self.withStubProtocol { recorder in StepFunStubURLProtocol.handler = { request in let path = request.url?.path ?? "" if path.contains("QueryStepPlanRateLimit") { let call = recorder.recordUsageCall() if call == 1 { #expect(request.value(forHTTPHeaderField: "Cookie")? .contains("stale-access...stale-refresh") == true) return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"unauthorized"}"#) } #expect(request.value(forHTTPHeaderField: "Cookie")?.contains("env-access...env-refresh") == true) return Self.usageResponse(for: request) } if path.contains("RefreshToken") { recorder.recordRefreshCall() return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"expired"}"#) } if path.contains("GetStepPlanStatus") { return Self.jsonResponse( for: request, body: #"{"status":1,"subscription":{"name":"Plus","plan_type":1,"status":1}}"#) } return Self.jsonResponse(for: request, statusCode: 404, body: #"{"error":"unexpected"}"#) } let settings = ProviderSettingsSnapshot.make( stepfun: ProviderSettingsSnapshot.StepFunProviderSettings(cookieSource: .auto)) let context = self.makeContext( settings: settings, env: ["STEPFUN_TOKEN": "env-access...env-refresh"]) _ = try await StepFunWebFetchStrategy().fetch(context) #expect(recorder.usageCallCount == 2) #expect(recorder.refreshCallCount == 1) #expect(CookieHeaderCache.load(provider: .stepfun) == nil) } } @Test func `stale cached and env tokens fall back to env login credentials`() async throws { CookieHeaderCache.store(provider: .stepfun, cookieHeader: "stale-access...stale-refresh", sourceLabel: "test") defer { CookieHeaderCache.clear(provider: .stepfun) } try await self.withStubProtocol { recorder in StepFunStubURLProtocol.handler = { request in let path = request.url?.path ?? "" if path.isEmpty || path == "/" { return Self.jsonResponse( for: request, body: "{}", headers: ["Set-Cookie": "INGRESSCOOKIE=ingress-cookie; Path=/"]) } if path.contains("RegisterDevice") { return Self.jsonResponse( for: request, body: """ { "accessToken": {"raw": "anon-access"}, "refreshToken": {"raw": "anon-refresh"} } """) } if path.contains("SignInByPassword") { return Self.jsonResponse( for: request, body: """ { "accessToken": {"raw": "login-access"}, "refreshToken": {"raw": "login-refresh"} } """) } if path.contains("QueryStepPlanRateLimit") { let call = recorder.recordUsageCall() if call == 1 { #expect(request.value(forHTTPHeaderField: "Cookie")? .contains("stale-access...stale-refresh") == true) return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"unauthorized"}"#) } if call == 2 { #expect(request.value(forHTTPHeaderField: "Cookie")? .contains("env-access...env-refresh") == true) return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"unauthorized"}"#) } #expect(request.value(forHTTPHeaderField: "Cookie")? .contains("login-access...login-refresh") == true) return Self.usageResponse(for: request) } if path.contains("RefreshToken") { recorder.recordRefreshCall() return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"expired"}"#) } if path.contains("GetStepPlanStatus") { return Self.jsonResponse( for: request, body: #"{"status":1,"subscription":{"name":"Plus","plan_type":1,"status":1}}"#) } return Self.jsonResponse(for: request, statusCode: 404, body: #"{"error":"unexpected"}"#) } let settings = ProviderSettingsSnapshot.make( stepfun: ProviderSettingsSnapshot.StepFunProviderSettings(cookieSource: .auto)) let context = self.makeContext( settings: settings, env: [ "STEPFUN_TOKEN": "env-access...env-refresh", "STEPFUN_USERNAME": "user@example.com", "STEPFUN_PASSWORD": "password", ]) _ = try await StepFunWebFetchStrategy().fetch(context) #expect(recorder.usageCallCount == 3) #expect(recorder.refreshCallCount == 1) #expect(CookieHeaderCache.load(provider: .stepfun)?.cookieHeader == "login-access...login-refresh") } } @Test func `post refresh non auth usage failure is not rewritten as auth guidance`() async throws { try await self.withStubProtocol { recorder in StepFunStubURLProtocol.handler = { request in let path = request.url?.path ?? "" if path.contains("QueryStepPlanRateLimit") { let call = recorder.recordUsageCall() if call == 1 { return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"unauthorized"}"#) } return Self.jsonResponse(for: request, statusCode: 500, body: #"{"error":"temporary"}"#) } if path.contains("RefreshToken") { recorder.recordRefreshCall() return Self.jsonResponse( for: request, body: """ { "accessToken": {"raw": "new-access"}, "refreshToken": {"raw": "new-refresh"} } """) } return Self.jsonResponse(for: request, statusCode: 404, body: #"{"error":"unexpected"}"#) } let settings = ProviderSettingsSnapshot.make( stepfun: ProviderSettingsSnapshot.StepFunProviderSettings( cookieSource: .manual, manualToken: "old-access...old-refresh")) let context = self.makeContext(settings: settings) do { _ = try await StepFunWebFetchStrategy().fetch(context) Issue.record("Expected post-refresh usage failure") } catch let StepFunUsageError.apiError(message) { #expect(message == "HTTP 500") } catch { Issue.record("Expected StepFunUsageError.apiError, got \(error)") } #expect(recorder.usageCallCount == 2) #expect(recorder.refreshCallCount == 1) } } @Test func `manual token refresh failure does not fall back to ambient env credentials`() async throws { try await self.withStubProtocol { recorder in StepFunStubURLProtocol.handler = { request in let path = request.url?.path ?? "" if path.contains("QueryStepPlanRateLimit") { _ = recorder.recordUsageCall() return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"unauthorized"}"#) } if path.contains("RefreshToken") { recorder.recordRefreshCall() return Self.jsonResponse(for: request, statusCode: 401, body: #"{"error":"expired"}"#) } Issue.record("Manual token recovery should not call login endpoint: \(path)") return Self.jsonResponse(for: request, statusCode: 404, body: #"{"error":"unexpected"}"#) } let settings = ProviderSettingsSnapshot.make( stepfun: ProviderSettingsSnapshot.StepFunProviderSettings( cookieSource: .manual, manualToken: "old-access...old-refresh")) let context = self.makeContext( settings: settings, env: [ "STEPFUN_USERNAME": "someone@example.com", "STEPFUN_PASSWORD": "secret", ]) do { _ = try await StepFunWebFetchStrategy().fetch(context) Issue.record("Expected manual token auth failure") } catch let StepFunUsageError.apiError(message) { #expect(message.contains("Refresh the Oasis-Token")) } catch { Issue.record("Expected StepFunUsageError.apiError, got \(error)") } #expect(recorder.usageCallCount == 1) #expect(recorder.refreshCallCount == 1) } } @Test func `non auth token wording does not trigger refresh recovery`() async throws { try await self.withStubProtocol { recorder in StepFunStubURLProtocol.handler = { request in let path = request.url?.path ?? "" if path.contains("QueryStepPlanRateLimit") { _ = recorder.recordUsageCall() return Self.jsonResponse( for: request, body: #"{"status":0,"message":"token plan status temporarily unavailable"}"#) } Issue.record("Non-auth usage error should not call recovery endpoint: \(path)") return Self.jsonResponse(for: request, statusCode: 404, body: #"{"error":"unexpected"}"#) } let settings = ProviderSettingsSnapshot.make( stepfun: ProviderSettingsSnapshot.StepFunProviderSettings( cookieSource: .manual, manualToken: "old-access...old-refresh")) let context = self.makeContext(settings: settings) do { _ = try await StepFunWebFetchStrategy().fetch(context) Issue.record("Expected provider API error") } catch let StepFunUsageError.apiError(message) { #expect(message == "token plan status temporarily unavailable") } catch { Issue.record("Expected StepFunUsageError.apiError, got \(error)") } #expect(recorder.usageCallCount == 1) #expect(recorder.refreshCallCount == 0) } } private func makeContext( settings: ProviderSettingsSnapshot?, env: [String: String] = [:], selectedTokenAccountID: UUID? = nil, tokenUpdater: ProviderFetchContext.TokenAccountTokenUpdater? = nil, manualTokenUpdater: ProviderFetchContext.ProviderManualTokenUpdater? = nil) -> ProviderFetchContext { ProviderFetchContext( runtime: .app, sourceMode: .auto, includeCredits: false, webTimeout: 1, webDebugDumpHTML: false, verbose: false, env: env, settings: settings, fetcher: UsageFetcher(environment: [:]), claudeFetcher: StubClaudeFetcher(), browserDetection: BrowserDetection(cacheTTL: 0), selectedTokenAccountID: selectedTokenAccountID, tokenAccountTokenUpdater: tokenUpdater, providerManualTokenUpdater: manualTokenUpdater) } private func withStubProtocol( _ body: (StepFunRequestRecorder) async throws -> Void) async throws { let recorder = StepFunRequestRecorder() let registered = URLProtocol.registerClass(StepFunStubURLProtocol.self) defer { if registered { URLProtocol.unregisterClass(StepFunStubURLProtocol.self) } StepFunStubURLProtocol.handler = nil } try await body(recorder) } private static func usageResponse(for request: URLRequest) -> (HTTPURLResponse, Data) { self.jsonResponse( for: request, body: """ { "status": 1, "five_hour_usage_left_rate": 0.8, "weekly_usage_left_rate": 0.6, "five_hour_usage_reset_time": "1777528800", "weekly_usage_reset_time": "1777899600" } """) } private static func jsonResponse( for request: URLRequest, statusCode: Int = 200, body: String, headers: [String: String] = ["Content-Type": "application/json"]) -> (HTTPURLResponse, Data) { let response = HTTPURLResponse( url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: headers)! return (response, Data(body.utf8)) } } private actor StepFunTokenUpdateRecorder { private var token: String? func record(_ token: String) { self.token = token } func recordedToken() -> String? { self.token } } private final class StepFunRequestRecorder: @unchecked Sendable { private let lock = NSLock() private var usageCalls = 0 private var refreshCalls = 0 var usageCallCount: Int { self.lock.lock() defer { self.lock.unlock() } return self.usageCalls } var refreshCallCount: Int { self.lock.lock() defer { self.lock.unlock() } return self.refreshCalls } func recordUsageCall() -> Int { self.lock.lock() defer { self.lock.unlock() } self.usageCalls += 1 return self.usageCalls } func recordRefreshCall() { self.lock.lock() defer { self.lock.unlock() } self.refreshCalls += 1 } } private final class StepFunStubURLProtocol: URLProtocol { nonisolated(unsafe) static var handler: (@Sendable (URLRequest) throws -> (HTTPURLResponse, Data))? override static func canInit(with request: URLRequest) -> Bool { request.url?.host == "platform.stepfun.com" } override static func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { 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() {} }