diff --git a/.circleci/config.yml b/.circleci/config.yml index 3139bd3cb2..38fdaf3609 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2477,10 +2477,15 @@ jobs: DISABLE_SCHEMA_UPDATE: "true" SERVER_ROOT_PATH: "" PROXY_LOGOUT_URL: "" + # LITELLM_LICENSE is forwarded from the project env so premium-gated + # UI flows can be exercised. license.spec.ts asserts the resulting + # JWT carries premium_user=true; if it ever stops being passed, that + # test fails loudly rather than silently regressing premium coverage. command: | - uv run --no-sync python -m litellm.proxy.proxy_cli \ - --config ui/litellm-dashboard/e2e_tests/fixtures/config.yml \ - --port 4000 + LITELLM_LICENSE="$LITELLM_LICENSE" \ + uv run --no-sync python -m litellm.proxy.proxy_cli \ + --config ui/litellm-dashboard/e2e_tests/fixtures/config.yml \ + --port 4000 background: true - run: name: Wait for proxy to be ready @@ -2497,9 +2502,12 @@ jobs: exit 1 - run: name: Run Playwright E2E tests + # Forward LITELLM_LICENSE so license.spec.ts can detect that the + # proxy was launched with a license and assert premium_user=true. command: | cd ui/litellm-dashboard - npx playwright test --config e2e_tests/playwright.config.ts + LITELLM_LICENSE="$LITELLM_LICENSE" \ + npx playwright test --config e2e_tests/playwright.config.ts no_output_timeout: 10m - store_artifacts: path: ui/litellm-dashboard/test-results diff --git a/ui/litellm-dashboard/e2e_tests/run_e2e.sh b/ui/litellm-dashboard/e2e_tests/run_e2e.sh index f8f570cda8..36619dce9b 100755 --- a/ui/litellm-dashboard/e2e_tests/run_e2e.sh +++ b/ui/litellm-dashboard/e2e_tests/run_e2e.sh @@ -95,6 +95,10 @@ export DISABLE_SCHEMA_UPDATE="true" export SERVER_ROOT_PATH="" # Prevent logout from redirecting to an external URL export PROXY_LOGOUT_URL="" +# Forward LITELLM_LICENSE if set in the outer env so premium-gated UI flows +# (e.g. Team-BYOK Model switch) can be exercised. Tests that depend on a +# premium proxy gate themselves on process.env.LITELLM_LICENSE. +export LITELLM_LICENSE="${LITELLM_LICENSE:-}" # --- Rebuild UI from source --- echo "=== Building UI from source ===" diff --git a/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/license.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/license.spec.ts new file mode 100644 index 0000000000..579b3cede7 --- /dev/null +++ b/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/license.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from "@playwright/test"; +import * as fs from "fs"; +import { ADMIN_STORAGE_PATH } from "../../constants"; + +/** + * Sanity check that LITELLM_LICENSE is being forwarded to the proxy when set + * in the environment (e.g. CircleCI's `e2e_ui_testing` job). The login JWT's + * `premium_user` claim is the same value the dashboard reads to enable + * premium-gated UI surfaces (Team-BYOK switch, etc.), so asserting it here + * catches any future regression where the env var stops being plumbed + * through `run_e2e.sh` / `.circleci/config.yml`. + * + * Skips locally when no license is configured. + */ +test.describe("Premium license wiring", () => { + test("admin session JWT carries premium_user=true when LITELLM_LICENSE is set", () => { + test.skip( + !process.env.LITELLM_LICENSE, + "LITELLM_LICENSE not set in test env — proxy is running unlicensed", + ); + + const storage = JSON.parse(fs.readFileSync(ADMIN_STORAGE_PATH, "utf-8")); + const tokenCookie = storage.cookies?.find((c: { name: string }) => c.name === "token"); + expect(tokenCookie, "token cookie missing from admin storage state").toBeDefined(); + + // Decode the JWT payload (no signature check — we trust globalSetup ran + // against our own proxy). Payload is the middle base64url segment. + const jwtParts = tokenCookie.value.split("."); + expect(jwtParts.length, "token cookie is not a 3-part JWT").toBe(3); + const [, payloadB64] = jwtParts; + const payload = JSON.parse( + Buffer.from(payloadB64, "base64url").toString("utf-8"), + ); + + expect(payload.premium_user).toBe(true); + }); +});