| 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as TestClock from "effect/testing/TestClock"; |
| 4 | |
| 5 | import * as GitHubQuota from "./GitHubQuota.ts"; |
| 6 | import { CredentialScope } from "@t3tools/source-control-core/server/SourceControlRateLimit"; |
| 7 | |
| 8 | const RESET = Date.parse("2099-08-13T14:00:00Z"); |
| 9 | const headers = (remaining: number, resource = "graphql", reset = RESET) => ({ |
| 10 | "x-ratelimit-resource": resource, |
| 11 | "x-ratelimit-limit": "5000", |
| 12 | "x-ratelimit-remaining": String(remaining), |
| 13 | "x-ratelimit-reset": String(reset / 1000), |
| 14 | }); |
| 15 | |
| 16 | describe("GitHubQuota", () => { |
| 17 | it.effect("refuses a background request below the reserve, per quota and account", () => |
| 18 | Effect.gen(function* () { |
| 19 | const quota = yield* GitHubQuota.GitHubQuota; |
| 20 | yield* quota.observe("github.com", headers(499)); |
| 21 | const refused = yield* Effect.flip(quota.admit("github.com", "graphql")); |
| 22 | assert.strictEqual(refused.retryAt, RESET); |
| 23 | // Another quota, another host and another account each have their own balance. |
| 24 | yield* quota.admit("github.com", "core"); |
| 25 | yield* quota.admit("ghe.example", "graphql"); |
| 26 | yield* quota.admit("github.com", "graphql").pipe(Effect.provideService(CredentialScope, "b")); |
| 27 | yield* quota.admit("github.com", "graphql", { allowReserve: true }); |
| 28 | }).pipe(Effect.provide(GitHubQuota.layer)), |
| 29 | ); |
| 30 | |
| 31 | it.effect("takes the latest balance in a window, and ignores an older window", () => |
| 32 | Effect.gen(function* () { |
| 33 | const quota = yield* GitHubQuota.GitHubQuota; |
| 34 | yield* quota.observe("github.com", headers(400)); |
| 35 | yield* Effect.flip(quota.admit("github.com", "graphql")); |
| 36 | // GitHub can report more left later in the same window, from another region. |
| 37 | yield* quota.observe("github.com", headers(900)); |
| 38 | yield* quota.admit("github.com", "graphql"); |
| 39 | yield* quota.observe("github.com", headers(400)); |
| 40 | yield* Effect.flip(quota.admit("github.com", "graphql")); |
| 41 | // An answer from an older window says nothing about this one. |
| 42 | yield* quota.observe("github.com", headers(4000, "graphql", RESET - 3_600_000)); |
| 43 | yield* Effect.flip(quota.admit("github.com", "graphql")); |
| 44 | // A later window replaces it. |
| 45 | yield* quota.observe("github.com", headers(4000, "graphql", RESET + 3_600_000)); |
| 46 | yield* quota.admit("github.com", "graphql"); |
| 47 | }).pipe(Effect.provide(GitHubQuota.layer)), |
| 48 | ); |
| 49 | |
| 50 | it.effect("lets interactive requests spend the reserve but not an empty quota", () => |
| 51 | Effect.gen(function* () { |
| 52 | const quota = yield* GitHubQuota.GitHubQuota; |
| 53 | yield* quota.observe("github.com", headers(1)); |
| 54 | yield* quota.admit("github.com", "graphql", { allowReserve: true }); |
| 55 | yield* quota.observe("github.com", headers(0)); |
| 56 | const refused = yield* Effect.flip( |
| 57 | quota.admit("github.com", "graphql", { allowReserve: true }), |
| 58 | ); |
| 59 | assert.strictEqual(refused.retryAt, RESET); |
| 60 | }).pipe(Effect.provide(GitHubQuota.layer)), |
| 61 | ); |
| 62 | |
| 63 | it.effect("spends nothing locally, so a free 304 cannot run the balance down", () => |
| 64 | Effect.gen(function* () { |
| 65 | const quota = yield* GitHubQuota.GitHubQuota; |
| 66 | yield* quota.observe("github.com", headers(501, "core")); |
| 67 | // Conditional reads answer 304 with the balance unchanged, however many are sent. |
| 68 | for (let index = 0; index < 5; index++) { |
| 69 | yield* quota.admit("github.com", "core"); |
| 70 | yield* quota.observe("github.com", headers(501, "core")); |
| 71 | } |
| 72 | yield* quota.admit("github.com", "core"); |
| 73 | }).pipe(Effect.provide(GitHubQuota.layer)), |
| 74 | ); |
| 75 | |
| 76 | it.effect("forgets a balance once its window resets, and ignores answers naming no quota", () => |
| 77 | Effect.gen(function* () { |
| 78 | const quota = yield* GitHubQuota.GitHubQuota; |
| 79 | yield* quota.observe("github.com", headers(0)); |
| 80 | yield* TestClock.setTime(RESET); |
| 81 | yield* quota.admit("github.com", "graphql"); |
| 82 | yield* quota.observe("other.example", { "x-ratelimit-remaining": "0" }); |
| 83 | yield* quota.admit("other.example", "graphql"); |
| 84 | }).pipe(Effect.provide(GitHubQuota.layer)), |
| 85 | ); |
| 86 | }); |