355 lines · 12.6 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBar@testable import CodexBarCore struct ChutesProviderTests {    @Test    func `settings reader trims quoted API key`() {        let token = ChutesSettingsReader.apiKey(environment: [            ChutesSettingsReader.apiKeyEnvironmentKey: " 'chutes-test' ",        ])         #expect(token == "chutes-test")    }     @Test    func `config API key projects into Chutes environment`() {        let config = ProviderConfig(id: .chutes, apiKey: "chutes-config-token")        let env = ProviderConfigEnvironment.applyAPIKeyOverride(            base: [:],            provider: .chutes,            config: config)         #expect(env[ChutesSettingsReader.apiKeyEnvironmentKey] == "chutes-config-token")        #expect(ChutesSettingsReader.apiKey(environment: env) == "chutes-config-token")        #expect(ProviderConfigEnvironment.supportsAPIKeyOverride(for: .chutes))    }     @Test    func `fetch usage maps active subscription monthly and rolling windows`() async throws {        let now = Date(timeIntervalSince1970: 1_800_000_000)        let rollingReset = try Self.date("2026-06-13T18:00:00Z")        let monthlyReset = try Self.date("2026-07-01T00:00:00Z")        let transport = ProviderHTTPTransportStub { request in            let url = try #require(request.url)            #expect(url.path == "/users/me/subscription_usage")            #expect(request.httpMethod == "GET")            #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer chutes-key")            #expect(request.value(forHTTPHeaderField: "Accept") == "application/json")            #expect(request.timeoutInterval == 15)             let body = #"""            {              "subscription": {                "active": true,                "plan_name": "Pro",                "current_period_end": "2026-07-01T00:00:00Z"              },              "monthly": {                "used": 250,                "limit": 1000,                "resets_at": "2026-07-01T00:00:00Z",                "unit": "credits"              },              "rolling_window": {                "requests": 40,                "limit": 100,                "window_minutes": 240,                "reset_at": "2026-06-13T18:00:00Z",                "unit": "requests"              }            }            """#            return Self.makeResponse(url: url, body: body)        }         let snapshot = try await ChutesUsageFetcher.fetchUsage(            apiKey: " chutes-key ",            environment: [ChutesSettingsReader.apiURLEnvironmentKey: "https://chutes.test"],            transport: transport,            now: now)        let usage = snapshot.toUsageSnapshot()         #expect(usage.primary?.usedPercent == 40)        #expect(usage.primary?.windowMinutes == 240)        #expect(usage.primary?.resetsAt == rollingReset)        #expect(usage.primary?.resetDescription == "40/100 requests")        #expect(usage.secondary?.usedPercent == 25)        #expect(usage.secondary?.resetsAt == monthlyReset)        #expect(usage.secondary?.resetDescription == "250/1000 credits")        #expect(usage.subscriptionRenewsAt == monthlyReset)        #expect(usage.loginMethod(for: .chutes) == "Pro")         let requests = await transport.requests()        #expect(requests.count == 1)    }     @Test    func `no active subscription falls back to quotas endpoint`() async throws {        let now = Date(timeIntervalSince1970: 1_800_000_000)        let transport = ProviderHTTPTransportStub { request in            let url = try #require(request.url)            switch url.path {            case "/users/me/subscription_usage":                return Self.makeResponse(url: url, body: #"""                {                  "subscription": {                    "active": false,                    "status": "free"                  }                }                """#)            case "/users/me/quotas":                return Self.makeResponse(url: url, body: #"""                [                  {                    "chute_id": "0",                    "is_default": true,                    "quota": 100                  }                ]                """#)            case "/users/me/quota_usage/0":                return Self.makeResponse(url: url, body: #"""                {                  "quota": 100,                  "used": 10                }                """#)            default:                throw URLError(.badURL)            }        }         let snapshot = try await ChutesUsageFetcher.fetchUsage(            apiKey: "chutes-key",            environment: [ChutesSettingsReader.apiURLEnvironmentKey: "https://chutes.test"],            transport: transport,            now: now)        let usage = snapshot.toUsageSnapshot()         #expect(usage.primary?.usedPercent == 10)        #expect(usage.primary?.resetDescription == "10/100 credits")        #expect(usage.secondary == nil)        #expect(usage.loginMethod(for: .chutes) == "No active subscription")         let requests = await transport.requests()        let paths = requests.compactMap { $0.url?.path }        #expect(paths == [            "/users/me/subscription_usage",            "/users/me/quotas",            "/users/me/quota_usage/0",        ])    }     @Test    func `wrapped quota list fetches per quota usage`() async throws {        let transport = ProviderHTTPTransportStub { request in            let url = try #require(request.url)            switch url.path {            case "/users/me/subscription_usage":                return Self.makeResponse(url: url, body: #"{"subscription":{"active":false}}"#)            case "/users/me/quotas":                return Self.makeResponse(url: url, body: #"""                {                  "data": [                    {                      "chute_id": "wrapped",                      "quota": 200                    }                  ]                }                """#)            case "/users/me/quota_usage/wrapped":                return Self.makeResponse(url: url, body: #"{"quota":200,"used":50}"#)            default:                throw URLError(.badURL)            }        }         let snapshot = try await ChutesUsageFetcher.fetchUsage(            apiKey: "chutes-key",            environment: [ChutesSettingsReader.apiURLEnvironmentKey: "https://chutes.test"],            transport: transport)         #expect(snapshot.toUsageSnapshot().primary?.usedPercent == 25)        let requests = await transport.requests()        #expect(requests.compactMap { $0.url?.path } == [            "/users/me/subscription_usage",            "/users/me/quotas",            "/users/me/quota_usage/wrapped",        ])    }     @Test    func `partial subscription usage fills missing rolling window from quotas`() async throws {        let now = Date(timeIntervalSince1970: 1_800_000_000)        let transport = ProviderHTTPTransportStub { request in            let url = try #require(request.url)            switch url.path {            case "/users/me/subscription_usage":                return Self.makeResponse(url: url, body: #"""                {                  "subscription": {                    "active": true,                    "plan_name": "Pro",                    "current_period_end": "2026-07-01T00:00:00Z"                  },                  "monthly": {                    "used": 250,                    "limit": 1000,                    "unit": "credits"                  }                }                """#)            case "/users/me/quotas":                return Self.makeResponse(url: url, body: #"""                {                  "rolling_window": {                    "requests": 40,                    "limit": 100,                    "window_minutes": 240,                    "unit": "requests"                  }                }                """#)            default:                throw URLError(.badURL)            }        }         let snapshot = try await ChutesUsageFetcher.fetchUsage(            apiKey: "chutes-key",            environment: [ChutesSettingsReader.apiURLEnvironmentKey: "https://chutes.test"],            transport: transport,            now: now)        let usage = snapshot.toUsageSnapshot()         #expect(usage.primary?.usedPercent == 40)        #expect(usage.primary?.windowMinutes == 240)        #expect(usage.primary?.resetDescription == "40/100 requests")        #expect(usage.secondary?.usedPercent == 25)        #expect(usage.secondary?.resetDescription == "250/1000 credits")        #expect(usage.loginMethod(for: .chutes) == "Pro")         let requests = await transport.requests()        let paths = requests.compactMap { $0.url?.path }        #expect(paths == ["/users/me/subscription_usage", "/users/me/quotas"])    }     @Test    func `missing usage fields returns no data snapshot without decode failure`() throws {        let data = Data(#"{"subscription":{"active":true},"unexpected":{"nested":true}}"#.utf8)        let snapshot = try ChutesUsageParser.parse(data: data, now: Date(timeIntervalSince1970: 123))        let usage = snapshot.toUsageSnapshot()         #expect(!snapshot.hasUsageData)        #expect(usage.primary == nil)        #expect(usage.secondary == nil)        #expect(usage.loginMethod(for: .chutes) == nil)    }     @Test    func `identical usage values keep distinct quota windows`() throws {        let data = Data(#"""        {          "quotas": [            {              "used": 0,              "limit": 100,              "window_minutes": 240            },            {              "used": 0,              "limit": 100,              "window_minutes": 43200            }          ]        }        """#.utf8)         let snapshot = try ChutesUsageParser.parse(data: data, now: Date(timeIntervalSince1970: 123))        let usage = snapshot.toUsageSnapshot()         #expect(usage.primary?.usedPercent == 0)        #expect(usage.primary?.windowMinutes == 240)        #expect(usage.secondary?.usedPercent == 0)        #expect(usage.secondary?.windowMinutes == 43200)    }     @Test    func `exact percent value of one stays one percent`() throws {        let usedData = Data(#"""        {          "rolling_window": {            "usage_percent": 1          }        }        """#.utf8)        let remainingData = Data(#"""        {          "rolling_window": {            "percent_remaining": 1          }        }        """#.utf8)         let usedSnapshot = try ChutesUsageParser.parse(            data: usedData,            now: Date(timeIntervalSince1970: 123))        let remainingSnapshot = try ChutesUsageParser.parse(            data: remainingData,            now: Date(timeIntervalSince1970: 123))         #expect(usedSnapshot.toUsageSnapshot().primary?.usedPercent == 1)        #expect(remainingSnapshot.toUsageSnapshot().primary?.usedPercent == 99)    }     @Test    func `auth failure surfaces invalid credentials`() async {        let transport = ProviderHTTPTransportStub { request in            let url = try #require(request.url)            return Self.makeResponse(url: url, body: #"{"detail":"unauthorized"}"#, statusCode: 401)        }         await #expect {            _ = try await ChutesUsageFetcher.fetchUsage(                apiKey: "bad-key",                environment: [ChutesSettingsReader.apiURLEnvironmentKey: "https://chutes.test"],                transport: transport)        } throws: { error in            guard case ChutesUsageError.invalidCredentials = error else { return false }            return true        }    }     @Test    func `descriptor and app implementation registry include Chutes`() throws {        let descriptor = ProviderDescriptorRegistry.descriptor(for: .chutes)        #expect(descriptor.metadata.displayName == "Chutes")        #expect(ProviderDescriptorRegistry.all.contains { $0.id == .chutes })         let implementation = try #require(ProviderImplementationRegistry.implementation(for: .chutes))        #expect(implementation is ChutesProviderImplementation)    }     private static func makeResponse(        url: URL,        body: String,        statusCode: Int = 200) -> (Data, URLResponse)    {        let response = HTTPURLResponse(            url: url,            statusCode: statusCode,            httpVersion: "HTTP/1.1",            headerFields: ["Content-Type": "application/json"])!        return (Data(body.utf8), response)    }     private static func date(_ text: String) throws -> Date {        let formatter = ISO8601DateFormatter()        formatter.formatOptions = [.withInternetDateTime]        return try #require(formatter.date(from: text))    }}