mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
* feat(ui): configure Anthropic workload identity federation from the dashboard Add Credential and Edit Credential offer workload identity federation for Anthropic, Add Model creates a federated credential and attaches it, and the credentials table marks federated credentials. Editing a credential now sends only the values the admin changed, and switching provider no longer leaves the previous provider's default base URL on screen * fix(ui): keep a credential edit to what the admin set in the federation form * fix(ui): lock the provider in the federation dialog opened from Add Model * fix(ui): require one federation id when the identity source is the proxy environment * test(credentials): cover the Anthropic federation dashboard and credential routes Integration cells for the credential routes every dashboard shape writes (round trips, PATCH set and delete, malformed bodies, non-admin refusals, the token-file allowlist and exchange-host checks, every identity source through chat and messages against a scripted exchange, concurrent writes across two workers and a worker kill mid burst), plus Playwright specs for the Add Credential, Edit Credential and Add Model federation flows and the team-admin view. The owned proxies boot with a 2 s config reload so both workers serve a stored credential inside the fixture budget. * test(e2e): type the federation spec's captured bodies and clean up the Add Model deployment by its created id captureRequestBody and postAsMaster take a type parameter instead of returning Record<string, any>, the spec names the credential and model write shapes it captures, and the Add Model cell reads the deployment id from the /model/new response right after the click so a later failing check no longer leaves the deployment behind. --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
import { expect, Page } from "@playwright/test";
|
|
import { masterKey } from "./traffic";
|
|
|
|
/**
|
|
* Runs `action` and returns the parsed body of the first matching request.
|
|
*
|
|
* `action` is a callback so the listener is armed before the click; awaiting the
|
|
* click first lets the request go by, and the test then hangs until timeout.
|
|
*/
|
|
export async function captureRequestBody<T = Record<string, any>>(
|
|
page: Page,
|
|
match: { method: string; urlIncludes: string },
|
|
action: () => Promise<void>,
|
|
): Promise<T> {
|
|
const pending = page.waitForRequest(
|
|
(req) =>
|
|
req.method() === match.method && req.url().includes(match.urlIncludes),
|
|
);
|
|
await action();
|
|
const request = await pending;
|
|
return JSON.parse(request.postData() ?? "{}") as T;
|
|
}
|
|
|
|
/** Reads an endpoint as the master key, so a failure is bad data and not an expired UI token. */
|
|
export async function readBack<T = any>(
|
|
page: Page,
|
|
endpoint: string,
|
|
): Promise<T> {
|
|
const res = await page.request.get(endpoint, {
|
|
headers: { Authorization: `Bearer ${masterKey()}` },
|
|
});
|
|
expect(res.ok(), `GET ${endpoint}`).toBe(true);
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
type OperationOutcome =
|
|
| { readonly status: "success" }
|
|
| { readonly status: "failure"; readonly error: unknown };
|
|
|
|
type RunFailure =
|
|
| { readonly status: "action_failure"; readonly error: unknown }
|
|
| { readonly status: "cleanup_failure"; readonly error: unknown }
|
|
| {
|
|
readonly status: "action_and_cleanup_failure";
|
|
readonly actionError: unknown;
|
|
readonly cleanupError: unknown;
|
|
};
|
|
|
|
function toRunFailure(
|
|
actionOutcome: OperationOutcome,
|
|
cleanupOutcome: OperationOutcome,
|
|
): RunFailure | null {
|
|
if (
|
|
actionOutcome.status === "failure" &&
|
|
cleanupOutcome.status === "failure"
|
|
) {
|
|
return {
|
|
status: "action_and_cleanup_failure",
|
|
actionError: actionOutcome.error,
|
|
cleanupError: cleanupOutcome.error,
|
|
};
|
|
}
|
|
if (actionOutcome.status === "failure") {
|
|
return { status: "action_failure", error: actionOutcome.error };
|
|
}
|
|
if (cleanupOutcome.status === "failure") {
|
|
return { status: "cleanup_failure", error: cleanupOutcome.error };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function raiseRunFailure(failure: RunFailure): never {
|
|
switch (failure.status) {
|
|
case "action_failure":
|
|
throw failure.error;
|
|
case "cleanup_failure":
|
|
throw failure.error;
|
|
case "action_and_cleanup_failure":
|
|
throw new AggregateError(
|
|
[failure.actionError, failure.cleanupError],
|
|
"Action and cleanup failed",
|
|
);
|
|
}
|
|
}
|
|
|
|
async function runAction(
|
|
action: () => void | Promise<void>,
|
|
): Promise<OperationOutcome> {
|
|
return Promise.resolve()
|
|
.then(action)
|
|
.then(
|
|
() => ({ status: "success" as const }),
|
|
(error: unknown) => ({ status: "failure" as const, error }),
|
|
);
|
|
}
|
|
|
|
async function runCleanup(
|
|
cleanup: () => boolean | Promise<boolean>,
|
|
): Promise<OperationOutcome> {
|
|
return Promise.resolve()
|
|
.then(cleanup)
|
|
.then(
|
|
(succeeded) =>
|
|
succeeded
|
|
? { status: "success" as const }
|
|
: {
|
|
status: "failure" as const,
|
|
error: new Error("Failed to clean up UI E2E resource"),
|
|
},
|
|
(error: unknown) => ({ status: "failure" as const, error }),
|
|
);
|
|
}
|
|
|
|
export async function runWithCleanup(
|
|
action: () => void | Promise<void>,
|
|
cleanup: () => boolean | Promise<boolean>,
|
|
): Promise<void> {
|
|
const actionOutcome = await runAction(action);
|
|
const cleanupOutcome = await runCleanup(cleanup);
|
|
const failure = toRunFailure(actionOutcome, cleanupOutcome);
|
|
if (failure !== null) raiseRunFailure(failure);
|
|
}
|