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

800 lines
30 KiB
Swift

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() {}
}