Complete managed account lifecycle
CI / test (push) Successful in 8m12s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 9m6s
CI / frontend (pull_request) Successful in 5m17s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m26s
CI / test (push) Successful in 8m12s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 9m6s
CI / frontend (pull_request) Successful in 5m17s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m26s
This commit was merged in pull request #63.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright-core";
|
||||
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
|
||||
|
||||
/* global document */
|
||||
|
||||
const ALL_ROLES = ["read", "control", "write", "administrator"];
|
||||
const fixture = await startWebFixture({
|
||||
authSession: {
|
||||
authenticated: true,
|
||||
username: "admin",
|
||||
roles: ALL_ROLES,
|
||||
auth_disabled: false,
|
||||
},
|
||||
users: [
|
||||
{ username: "admin", roles: ALL_ROLES, enabled: true },
|
||||
{ username: "listener", roles: ["read"], enabled: false },
|
||||
],
|
||||
});
|
||||
const { browser, page, runtimeErrors } = await startBrowser(chromium);
|
||||
|
||||
try {
|
||||
await page.goto(`${fixture.origin}/settings`, { waitUntil: "domcontentloaded" });
|
||||
await page.locator("#tab-settings").waitFor({ state: "visible" });
|
||||
await page.locator("#settings-account-tab").waitFor({ state: "visible" });
|
||||
await page.locator("#settings-users-tab").waitFor({ state: "visible" });
|
||||
|
||||
await page.locator("#settings-account-tab").click();
|
||||
assert.equal(await page.locator("#account-password-form").isVisible(), true);
|
||||
assert.equal(await page.locator("#subtab-settings-users").isVisible(), false);
|
||||
|
||||
await page.locator("#settings-users-tab").click();
|
||||
await page.locator("#user-list").getByText("listener (disabled)").waitFor();
|
||||
assert.equal(await page.locator("#user-create-form").isVisible(), true);
|
||||
|
||||
const state = await page.evaluate(() => {
|
||||
const rows = [...document.querySelectorAll("#user-list > .sch-row")];
|
||||
const rowFor = (username) => rows.find((row) => row.querySelector("strong")?.textContent.startsWith(username));
|
||||
const admin = rowFor("admin");
|
||||
const listener = rowFor("listener");
|
||||
const role = (row, value) => row?.querySelector(`input[value="${value}"]`);
|
||||
return {
|
||||
createRoles: [...document.querySelectorAll("#user-create-roles input")].map((input) => input.value),
|
||||
adminEnabledLocked: admin?.querySelector('input[type="checkbox"]')?.disabled,
|
||||
adminRoleLocked: role(admin, "administrator")?.disabled,
|
||||
adminRemoveLocked: admin?.querySelector("button.danger")?.disabled,
|
||||
listenerEnabled: listener?.querySelector('input[type="checkbox"]')?.checked,
|
||||
listenerRead: role(listener, "read")?.checked,
|
||||
};
|
||||
});
|
||||
assert.deepEqual(state.createRoles, ALL_ROLES);
|
||||
assert.equal(state.adminEnabledLocked, true);
|
||||
assert.equal(state.adminRoleLocked, true);
|
||||
assert.equal(state.adminRemoveLocked, true);
|
||||
assert.equal(state.listenerEnabled, false);
|
||||
assert.equal(state.listenerRead, true);
|
||||
assert.deepEqual(runtimeErrors, []);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await fixture.close();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import { bundleEntry } from "./bundle-entry.mjs";
|
||||
|
||||
const source = await bundleEntry(new URL("../src/api/auth.ts", import.meta.url), "AuthApi");
|
||||
|
||||
function loadAuth(fetch) {
|
||||
const context = vm.createContext({ fetch, console });
|
||||
new vm.Script(source).runInContext(context);
|
||||
return context.AuthApi;
|
||||
}
|
||||
|
||||
test("role policy is centralized and preserves the Control-to-Read implication", () => {
|
||||
const auth = loadAuth(async () => { throw new Error("unused"); });
|
||||
assert.deepEqual(Array.from(auth.AUTH_ROLES), ["read", "control", "write", "administrator"]);
|
||||
assert.equal(auth.hasAuthRole(["control"], "read"), true);
|
||||
assert.equal(auth.hasAuthRole(["control"], "write"), false);
|
||||
assert.equal(auth.hasAuthRole(["administrator"], "write"), true);
|
||||
});
|
||||
|
||||
test("auth responses normalize roles and require the managed-account lifecycle state", async () => {
|
||||
const replies = new Map([
|
||||
["/auth/session", { authenticated: true, roles: ["write", "read", "write"], username: "alice" }],
|
||||
["/auth/users", [{ username: "alice", roles: ["write", "read"], enabled: false }]],
|
||||
]);
|
||||
const auth = loadAuth(async (url) => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => replies.get(String(url)),
|
||||
}));
|
||||
|
||||
assert.deepEqual(Array.from((await auth.fetchAuthSession()).roles), ["read", "write"]);
|
||||
assert.deepEqual(Array.from((await auth.listUsers())[0].roles), ["read", "write"]);
|
||||
assert.equal((await auth.listUsers())[0].enabled, false);
|
||||
});
|
||||
|
||||
test("changing a password sends current and replacement credentials", async () => {
|
||||
let request;
|
||||
const auth = loadAuth(async (url, init) => {
|
||||
request = { url, init };
|
||||
return { ok: true, status: 200, json: async () => ({}) };
|
||||
});
|
||||
|
||||
await auth.changeOwnPassword("old-password", "new-password");
|
||||
assert.equal(request.url, "/auth/account/password");
|
||||
assert.equal(request.init.method, "PATCH");
|
||||
assert.deepEqual(JSON.parse(request.init.body), {
|
||||
current_password: "old-password",
|
||||
new_password: "new-password",
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -40,7 +40,7 @@ test("background decode loads configuration for the explicitly selected rig", as
|
||||
const source = await bundleEntry(new URL("../src/plugins/background-decode.ts", import.meta.url));
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
window.trx.modules.backgroundDecode.initialize("rig/a", "administrator");
|
||||
window.trx.modules.backgroundDecode.initialize("rig/a", ["administrator"]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.ok(requested.includes("/background-decode/rig%2Fa"));
|
||||
assert.ok(requested.includes("/bookmarks"));
|
||||
|
||||
@@ -32,7 +32,6 @@ function hostFixture(overrides = {}) {
|
||||
const calls = { postPath: [], setRigFrequency: [], armOptimisticFrequency: [], applyLocalTunedFrequency: [], syncBandwidthInput: [], scheduleSpectrumDraw: 0, syncModePicker: 0 };
|
||||
const state = {
|
||||
authEnabled: false,
|
||||
authRole: "administrator",
|
||||
authRoles: ["read", "control", "write", "administrator"],
|
||||
lastActiveRigId: null,
|
||||
lastRigIds: [],
|
||||
@@ -158,7 +157,7 @@ test("bookmark controls follow the host authentication state", async () => {
|
||||
if (!elements.has(id)) elements.set(id, new ElementFixture());
|
||||
return elements.get(id);
|
||||
};
|
||||
const { window } = hostFixture({ authEnabled: true, authRole: "read", authRoles: ["read"] });
|
||||
const { window } = hostFixture({ authEnabled: true, authRoles: ["read"] });
|
||||
const context = vm.createContext({
|
||||
window,
|
||||
document: documentFixture(element),
|
||||
@@ -172,7 +171,6 @@ test("bookmark controls follow the host authentication state", async () => {
|
||||
|
||||
assert.equal(element("bm-add-btn").style.display, "none");
|
||||
|
||||
window.trx.state.authRole = "write";
|
||||
window.trx.state.authRoles = ["read", "write"];
|
||||
await window.trx.modules.bookmarks.fetch("");
|
||||
assert.equal(element("bm-add-btn").style.display, "");
|
||||
|
||||
@@ -19,7 +19,6 @@ export function createHost({ state = {}, core = {}, modules = {} } = {}) {
|
||||
serverLat: null,
|
||||
serverLon: null,
|
||||
authEnabled: false,
|
||||
authRole: "administrator",
|
||||
authRoles: ["read", "control", "write", "administrator"],
|
||||
lastActiveRigId: null,
|
||||
lastRigIds: [],
|
||||
|
||||
@@ -11,7 +11,7 @@ import { bundleEntry } from "./bundle-entry.mjs";
|
||||
test("scheduler registers a typed module service without lifecycle globals", async () => {
|
||||
// No role known yet: the entry registers its service and waits for the
|
||||
// application to drive initialization.
|
||||
const window = { ...createHost({ state: { authRole: null } }), trxUi: { confirm: async () => true } };
|
||||
const window = { ...createHost({ state: { authEnabled: true, authRoles: [] } }), trxUi: { confirm: async () => true } };
|
||||
const context = vm.createContext({
|
||||
window,
|
||||
document: {
|
||||
@@ -76,7 +76,7 @@ test("scheduler self-initializes for the active rig when a role is already known
|
||||
return elements.get(id);
|
||||
};
|
||||
const window = {
|
||||
...createHost({ state: { authRole: "administrator", lastActiveRigId: "sdr" } }),
|
||||
...createHost({ state: { authRoles: ["administrator"], lastActiveRigId: "sdr" } }),
|
||||
trxUi: { confirm: async () => true },
|
||||
};
|
||||
const context = vm.createContext({
|
||||
|
||||
@@ -42,6 +42,18 @@ test("administrator user management is a dedicated Settings sub-tab", async () =
|
||||
assert.match(app, /settings-users-tab/);
|
||||
});
|
||||
|
||||
test("account lifecycle controls include self-service passwords and enable state", async () => {
|
||||
const [html, app] = await Promise.all([
|
||||
readFile(indexPath, "utf8"),
|
||||
readFile(appPath, "utf8"),
|
||||
]);
|
||||
assert.match(html, /data-subtab="settings-account"/);
|
||||
assert.match(html, /id="account-password-form"/);
|
||||
assert.match(html, /id="user-create-enabled"/);
|
||||
assert.match(app, /changeOwnPassword/);
|
||||
assert.match(app, /enabledAdminCount/);
|
||||
});
|
||||
|
||||
test("lazy frontend features use modules and local map symbols", async () => {
|
||||
const [loader, map] = await Promise.all([
|
||||
readFile(pluginLoaderPath, "utf8"),
|
||||
|
||||
@@ -145,6 +145,8 @@ export async function startWebFixture({
|
||||
bandplanEnabled = false,
|
||||
bandplanUnauthorizedFirst = false,
|
||||
satPasses = null,
|
||||
authSession = { authenticated: true, roles: ["read", "control", "write", "administrator"], auth_disabled: true },
|
||||
users = [],
|
||||
} = {}) {
|
||||
const rigItems = ["rig-a", "rig-b"].map((remote) => ({
|
||||
remote,
|
||||
@@ -232,7 +234,8 @@ export async function startWebFixture({
|
||||
};
|
||||
|
||||
const jsonRoutes = new Map([
|
||||
["/auth/session", { authenticated: true, roles: ["read", "control", "write", "administrator"], auth_disabled: true }],
|
||||
["/auth/session", authSession],
|
||||
["/auth/users", users],
|
||||
["/decoders", DECODER_REGISTRY],
|
||||
["/rigs", rigsResponse],
|
||||
["/status", status],
|
||||
|
||||
Reference in New Issue
Block a user