Files
dashboard/e2e/tests/reverse-proxy-services-https.spec.ts
Maycon Santos 7653e3411c Merge NetBird cloud edition into the dashboard (#674)
Brings the unified dashboard into the open-source repo. Premium features
ship in the open code, gated at runtime via NETBIRD_CLOUD and
NETBIRD_LICENSED, with upgrade prompts for unlicensed self-hosted
deployments. Adds the cloud-only feature areas (billing, integrations,
MSP, traffic events, notifications) and the Playwright e2e suite.
2026-06-21 16:01:08 +02:00

276 lines
11 KiB
TypeScript

import { test, expect } from "../helpers/fixtures";
import { navigateTo } from "../helpers/auth";
import { generateRandomName } from "../helpers/utils";
import { deleteNetworksByPrefix, deleteServicesByPrefix } from "../helpers/api";
import { gotoReverseProxyPage, selectProxyDomain, CUSTOM_PORTS_DOMAIN } from "../helpers/reverse-proxy-l4";
let createdNetwork = "";
let createdResource = "";
let createdSubdomain = "";
test.describe.serial("Reverse Proxy - Services (HTTPS) @reverse-proxy", () => {
test("Should create a network with a resource", async ({ dashboardAsOwner: page }) => {
// Clean up leftovers from previous runs (unique prefix per protocol)
await deleteServicesByPrefix(page, "https-svc-");
await deleteNetworksByPrefix(page, "rp-https-net-");
await navigateTo(page, "/networks");
const name = generateRandomName("rp-https-net-");
createdNetwork = name;
await page.getByTestId("add-network").click();
await page.getByTestId("network-name-input").fill(name);
await page.getByTestId("submit-network").click();
await page.getByTestId("confirmation.confirm").click({ force: true });
// Add resource
const resName = generateRandomName("rp-resource-");
createdResource = resName;
await page.getByTestId("resource-name-input").fill(resName);
await page.getByTestId("resource-address-input").fill("10.99.99.10");
await page.getByTestId("resource-continue").click();
const responsePromise = page.waitForResponse(
(resp) =>
resp.url().includes("/api/networks/") &&
resp.url().includes("/resources") &&
resp.request().method() === "POST",
{ timeout: 30_000 },
);
await page.getByTestId("submit-resource").click();
await page.getByTestId("confirmation.confirm").click({ force: true });
await responsePromise;
await page.getByTestId("confirmation.cancel").click({ force: true });
});
test("Should create an HTTPS reverse proxy service with full configuration", async ({
dashboardAsOwner: page,
}) => {
test.setTimeout(60_000);
await gotoReverseProxyPage(page, "/reverse-proxy/services");
const subdomain = generateRandomName("https-svc-");
createdSubdomain = subdomain;
await page.getByTestId("add-service").first().click();
await expect(page.getByTestId("proxy-subdomain-input")).toBeVisible({ timeout: 10_000 });
// Step 1: Service
await page.getByTestId("proxy-subdomain-input").fill(subdomain);
await selectProxyDomain(page, CUSTOM_PORTS_DOMAIN);
// Add 2 targets: http with options, https with port
await addTarget(page, {
resourceName: createdResource,
protocol: "http",
timeout: "10s",
customHeader: { name: "X-Custom-Header", value: "custom-value" },
});
await addTarget(page, {
resourceName: createdResource,
location: "/secure",
protocol: "https",
port: 4433,
});
const targetsSection = page.getByText("HTTPS Targets").locator("..");
await expect(targetsSection.locator("table tbody tr")).toHaveCount(2);
await page.getByTestId("proxy-continue").click();
// Step 2: Authentication
await page.getByTestId("auth-sso-card").click();
await page.getByTestId("submit-sso").click();
await page.getByTestId("auth-password-card").click();
await page.getByTestId("password-input").fill("super-secret-pass");
await page.getByTestId("submit-password").click();
await page.getByTestId("auth-pin-card").click();
const pinInputs = page.locator('input[inputmode="numeric"][maxlength="1"]');
for (let i = 0; i < 6; i++) {
await pinInputs.nth(i).fill(String(i + 1), { force: true });
}
await page.getByTestId("submit-pin").click();
await page.getByTestId("auth-header-card").click();
await page.getByTestId("header-type-select").click();
await page.locator("[cmdk-list]").getByText("Basic Auth").click({ force: true });
await page.getByTestId("header-basic-username").fill("admin");
await page.getByTestId("header-basic-password").fill("admin-pass");
await page.getByTestId("submit-headers").click();
await page.getByTestId("proxy-continue").click();
// Step 3: Access Control
await page.getByTestId("add-access-rule").click();
await page.getByTestId("access-rule-0").getByText("Select country...").click();
await page.getByTestId("select-dropdown-search").fill("Germany");
await page.getByText("Germany (DE)").click({ force: true });
await page.getByTestId("add-access-rule").click();
await page.getByTestId("access-rule-1").getByTestId("access-rule-action").click();
await page.getByText("Block Only").click({ force: true });
await page.getByTestId("access-rule-1").getByTestId("access-rule-type").click();
await page.locator('[role="option"]').filter({ hasText: "IP Address" }).click({ force: true });
const ipInput = page.getByTestId("access-rule-1").getByTestId("access-rule-value");
await expect(ipInput).toBeVisible();
await ipInput.fill("85.203.15.42");
await page.getByTestId("proxy-continue").click();
// Step 4: Advanced Settings
await page.getByTestId("toggle-pass-host-header").click();
await page.getByTestId("toggle-rewrite-redirects").click();
await page.getByTestId("submit-service").click();
await expect(page.locator("tr").filter({ hasText: subdomain })).toBeVisible({ timeout: 30_000 });
});
test("Should edit the service, remove auth and rules, then delete", async ({
dashboardAsOwner: page,
}) => {
await resetServiceFilters(page);
await page.locator("tr").filter({ hasText: createdSubdomain }).getByTestId("service-actions").click({ force: true });
await page.getByTestId("edit-service").click({ force: true });
// Edit first target
const targetsSection = page.getByText("HTTPS Targets").locator("..");
await targetsSection.locator("table tbody tr").first().click({ force: true });
await page.getByTestId("target-location-input").fill("/new-location");
await page.getByTestId("submit-target").click();
// Remove second target
await targetsSection.locator("table tbody tr").filter({ hasText: "/secure" }).getByTestId("target-row-actions").click();
await page.getByTestId("remove-target").click();
await expect(targetsSection.locator("table tbody tr")).toHaveCount(1);
// Remove all auth methods — click Edit on each card, then Remove in the modal
await page.getByTestId("proxy-tab-auth").click({ force: true });
await removeAuthMethod(page, "auth-sso-card", "remove-sso");
await removeAuthMethod(page, "auth-password-card", "remove-password");
await removeAuthMethod(page, "auth-pin-card", "remove-pin");
await removeAuthMethod(page, "auth-header-card", "remove-headers");
// Remove access control rules
await page.getByTestId("proxy-tab-access-control").click({ force: true });
await page.getByTestId("remove-access-rule").last().click({ force: true });
await page.getByTestId("remove-access-rule").first().click({ force: true });
// Toggle advanced settings back
await page.getByTestId("proxy-tab-settings").click({ force: true });
await page.getByTestId("toggle-pass-host-header").click({ force: true });
await page.getByTestId("toggle-rewrite-redirects").click({ force: true });
// Save and wait for API response
const saveResponse = page.waitForResponse(
(resp) =>
resp.url().includes("/api/reverse-proxies/services") &&
resp.request().method() === "PUT",
{ timeout: 15_000 },
);
await page.getByTestId("proxy-save").click();
const confirmBtn = page.getByTestId("confirmation.confirm");
if (await confirmBtn.isVisible({ timeout: 3_000 }).catch(() => false)) {
await confirmBtn.click({ force: true });
}
await saveResponse;
// Verify no auth / no access rules: both cells now show a "0" count badge.
await resetServiceFilters(page);
const row = page.locator("tr").filter({ hasText: createdSubdomain });
await expect(row.locator("[data-auth-cell]")).toContainText("0", {
timeout: 15_000,
});
await expect(
row.locator("[data-access-control-cell]"),
).toContainText("0", { timeout: 15_000 });
// Delete the service
await row.getByTestId("service-actions").click({ force: true });
await page.getByTestId("delete-service").click({ force: true });
await page.getByTestId("confirmation.confirm").click({ force: true });
await expect(row).not.toBeVisible();
});
test("Should delete the network", async ({ dashboardAsOwner: page }) => {
await deleteNetworksByPrefix(page, createdNetwork);
});
});
async function resetServiceFilters(page: import("@playwright/test").Page) {
const resetBtn = page.getByTestId("reset-filters-and-search");
if (await resetBtn.isVisible().catch(() => false)) {
await resetBtn.click();
}
}
type AddTargetOptions = {
resourceName: string;
location?: string;
protocol?: "http" | "https";
port?: number;
timeout?: string;
customHeader?: { name: string; value: string };
};
async function addTarget(page: import("@playwright/test").Page, opts: AddTargetOptions) {
await page.getByTestId("add-target").scrollIntoViewIfNeeded();
await page.getByTestId("add-target").click();
await expect(page.getByTestId("group-selector-dropdown")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("group-selector-dropdown").click();
await page.locator('[role="tab"]').filter({ hasText: "Resources" }).click({ force: true });
const search = page.getByTestId("group-selector-dropdown-search");
await expect(search).toBeVisible({ timeout: 5_000 });
await search.fill(opts.resourceName);
await page.getByText(opts.resourceName).click({ force: true, timeout: 15_000 });
await expect(page.getByTestId("target-port-input")).toBeVisible({ timeout: 10_000 });
if (opts.location) {
await expect(page.getByTestId("target-location-input")).toBeEnabled({ timeout: 5_000 });
await page.getByTestId("target-location-input").fill(opts.location);
}
if (opts.protocol === "https") {
await page.getByTestId("target-protocol-select").click();
await page.locator("[cmdk-list]").getByText("https://").click({ force: true });
}
if (opts.port !== undefined) {
await page.getByTestId("target-port-input").fill(String(opts.port));
} else {
await page.getByTestId("target-port-input").fill("");
}
if (opts.timeout || opts.customHeader) {
await page.getByTestId("target-optional-settings").click();
if (opts.timeout) {
await page.getByTestId("target-timeout-input").fill(opts.timeout);
}
if (opts.customHeader) {
await page.getByTestId("add-custom-header").click();
await page.getByTestId("custom-header-name-0").fill(opts.customHeader.name);
await page.getByTestId("custom-header-value-0").fill(opts.customHeader.value);
}
}
await page.getByTestId("submit-target").click();
}
async function removeAuthMethod(
page: import("@playwright/test").Page,
cardTestId: string,
removeTestId: string,
) {
const card = page.getByTestId(cardTestId);
const removeBtn = page.getByTestId(removeTestId);
// Click the card to open the auth modal
await card.click();
await expect(removeBtn).toBeVisible();
await removeBtn.click();
// Wait for the modal to fully close — the remove button must disappear
// and the "Enabled" badge on the card should also disappear
await expect(removeBtn).not.toBeVisible();
await expect(card.getByText("Enabled")).not.toBeVisible();
}