evcc-io/tests/api-key.spec.ts
2026-06-06 11:14:46 +02:00

176 lines
6.1 KiB
TypeScript

import { test, expect, type Page, type Locator } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { expectModalHidden, expectModalVisible } from "./utils";
test.use({ baseURL: baseUrl() });
const BASIC = "basics.evcc.yaml";
const PASSWORD = "secret";
async function loginAndOpenApiKey(page: Page): Promise<Locator> {
await page.goto("/#/config");
const loginModal = page.getByTestId("login-modal");
await expectModalVisible(loginModal);
await loginModal.getByLabel("Administrator Password").fill(PASSWORD);
await loginModal.getByRole("button", { name: "Login" }).click();
await expectModalHidden(loginModal);
return openApiKeyModal(page);
}
async function openApiKeyModal(page: Page): Promise<Locator> {
await page.getByTestId("generalconfig-security").getByRole("button", { name: "edit" }).click();
const securityModal = page.getByTestId("security-modal");
await expectModalVisible(securityModal);
await securityModal.getByRole("button", { name: "Generate API Key" }).click();
const apiKeyModal = page.getByTestId("api-key-modal");
await expectModalVisible(apiKeyModal);
return apiKeyModal;
}
async function generateKey(
page: Page,
modal: Locator,
action: "Generate API Key" | "Regenerate API Key"
): Promise<string> {
if (action === "Regenerate API Key") {
page.once("dialog", (dialog) => dialog.accept());
}
await modal.getByLabel("Administrator Password").fill(PASSWORD);
await modal.getByRole("button", { name: action, exact: true }).click();
const keyInput = modal.getByLabel("API Key", { exact: true });
await expect(keyInput).toBeVisible();
const key = await keyInput.inputValue();
expect(key).toMatch(/^evcc_/);
expect(key.length).toBeGreaterThan(10);
return key;
}
test("generate first key", async ({ page }) => {
await start(BASIC, "password.sql", "");
const modal = await loginAndOpenApiKey(page);
await expect(modal.getByRole("button", { name: "Generate API Key" })).toBeVisible();
const key = await generateKey(page, modal, "Generate API Key");
expect(key).toMatch(/^evcc_/);
await modal.getByRole("button", { name: "Close" }).last().click();
await expectModalHidden(modal);
// closing reveal returns to security modal — now offering Regenerate
const securityModal = page.getByTestId("security-modal");
await expectModalVisible(securityModal);
await expect(securityModal.getByRole("button", { name: "Regenerate" })).toBeVisible();
await stop();
});
test("regenerate replaces old key", async ({ page, request }) => {
await start(BASIC, "password.sql", "");
const modal = await loginAndOpenApiKey(page);
const first = await generateKey(page, modal, "Generate API Key");
await modal.getByRole("button", { name: "Close" }).last().click();
await expectModalHidden(modal);
const securityModal = page.getByTestId("security-modal");
await expectModalVisible(securityModal);
await securityModal.getByRole("button", { name: "Regenerate" }).click();
await expectModalVisible(modal);
const second = await generateKey(page, modal, "Regenerate API Key");
await modal.getByRole("button", { name: "Close" }).last().click();
expect(second).not.toBe(first);
const oldRes = await request.get("/api/config/site", {
headers: { Authorization: `Bearer ${first}` },
});
expect(oldRes.status()).toBe(401);
const newRes = await request.get("/api/config/site", {
headers: { Authorization: `Bearer ${second}` },
});
expect(newRes.status()).toBe(200);
await stop();
});
test("api key authenticates protected endpoints and bypasses backup pw", async ({
page,
request,
}) => {
await start(BASIC, "password.sql", "");
const modal = await loginAndOpenApiKey(page);
const key = await generateKey(page, modal, "Generate API Key");
const ok = await request.get("/api/config/site", {
headers: { Authorization: `Bearer ${key}` },
});
expect(ok.status()).toBe(200);
// backup without X-Admin-Password, bypass via API key
const backup = await request.get("/api/db/backup", {
headers: { Authorization: `Bearer ${key}` },
});
expect(backup.status()).toBe(200);
expect(backup.headers()["content-disposition"] || "").toContain("evcc-backup-");
const unauth = await request.get("/api/config/site");
expect(unauth.status()).toBe(401);
await stop();
});
test("api key cannot rotate itself without admin password", async ({ page, request }) => {
await start(BASIC, "password.sql", "");
const modal = await loginAndOpenApiKey(page);
const key = await generateKey(page, modal, "Generate API Key");
const bad = await request.post("/api/auth/apikey", {
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
data: { password: "" },
});
expect(bad.status()).toBe(401);
const good = await request.post("/api/auth/apikey", {
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
data: { password: PASSWORD },
});
expect(good.status()).toBe(200);
const body = await good.json();
expect(body.key).toMatch(/^evcc_/);
expect(body.key).not.toBe(key);
await stop();
});
test("api key cannot change admin password without correct current", async ({ page, request }) => {
await start(BASIC, "password.sql", "");
const modal = await loginAndOpenApiKey(page);
const key = await generateKey(page, modal, "Generate API Key");
const bad = await request.put("/api/auth/password", {
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
data: { current: "", new: "anything" },
});
expect(bad.status()).toBe(400);
await stop();
});
test("disable-auth shows banner and disables actions", async ({ page }) => {
await start(BASIC, null, "--disable-auth");
await page.goto("/#/config");
await page.getByTestId("generalconfig-security").getByRole("button", { name: "edit" }).click();
const security = page.getByTestId("security-modal");
await expectModalVisible(security);
await expect(security.getByText(/Authentication is disabled/i)).toBeVisible();
await expect(security.getByRole("button", { name: "Update password" })).toBeDisabled();
await expect(security.getByRole("button", { name: "Generate API Key" })).toBeDisabled();
await stop();
});