test(bstein-home): cover account frontend
This commit is contained in:
parent
1348aa85a8
commit
3675302596
@ -302,16 +302,18 @@ export function useAccountDashboard() {
|
||||
const syncEnabled = Boolean(data.sync_enabled);
|
||||
const syncOk = Boolean(data.sync_ok);
|
||||
const syncError = data.sync_error || "";
|
||||
let syncWarning = "";
|
||||
if (!syncEnabled) {
|
||||
mailu.status = "updated";
|
||||
mailu.error = "Mail sync is not configured; password may not take effect until an admin sync runs.";
|
||||
syncWarning = "Mail sync is not configured; password may not take effect until an admin sync runs.";
|
||||
} else if (!syncOk) {
|
||||
mailu.status = "sync pending";
|
||||
mailu.error = syncError || "Mail sync did not confirm success yet. Try again in a moment.";
|
||||
syncWarning = syncError || "Mail sync did not confirm success yet. Try again in a moment.";
|
||||
} else {
|
||||
mailu.status = "updated";
|
||||
}
|
||||
await refreshOverview();
|
||||
if (syncWarning) mailu.error = syncWarning;
|
||||
} catch (err) {
|
||||
mailu.error = formatActionError(err, "Rotation failed");
|
||||
} finally {
|
||||
|
||||
359
testing/frontend/unit/account-dashboard.spec.js
Normal file
359
testing/frontend/unit/account-dashboard.spec.js
Normal file
@ -0,0 +1,359 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||
import { flushPromises, mount } from "@vue/test-utils";
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
import { auth } from "../../../frontend/src/auth.js";
|
||||
import { useAccountDashboard } from "../../../frontend/src/account/useAccountDashboard.js";
|
||||
import AccountView from "../../../frontend/src/views/AccountView.vue";
|
||||
|
||||
function jsonResponse(body, status = 200, statusText = "OK") {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
statusText,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function overview(overrides = {}) {
|
||||
return {
|
||||
mailu: { status: "ready", username: "ADA@BSTEIN.DEV", app_password: "mail-pass" },
|
||||
nextcloud_mail: { status: "synced", primary_email: "ADA@BSTEIN.DEV", account_count: 1, synced_at: "now" },
|
||||
wger: { status: "ready", username: "ADA@BSTEIN.DEV", password: "wger-pass", password_updated_at: "today" },
|
||||
firefly: { status: "ready", username: "ADA@BSTEIN.DEV", password: "firefly-pass", password_updated_at: "today" },
|
||||
vaultwarden: { status: "already_present", username: "ADA@BSTEIN.DEV", synced_at: "now" },
|
||||
jellyfin: { status: "ready", username: "ada", sync_status: "ok", sync_detail: "LDAP synced" },
|
||||
onboarding_url: "/onboarding?code=ada",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function installFetch(handler) {
|
||||
global.fetch = jest.fn(async (url, options = {}) => handler(String(url), options));
|
||||
}
|
||||
|
||||
function authenticate() {
|
||||
auth.ready = true;
|
||||
auth.enabled = true;
|
||||
auth.authenticated = true;
|
||||
auth.username = "ada";
|
||||
auth.email = "Ada@Example.Dev";
|
||||
auth.token = "token";
|
||||
auth.accountPasswordUrl = "https://sso.example.dev/account/password";
|
||||
}
|
||||
|
||||
function mountDashboard() {
|
||||
let dashboard;
|
||||
const wrapper = mount(defineComponent({
|
||||
setup() {
|
||||
dashboard = useAccountDashboard();
|
||||
return {};
|
||||
},
|
||||
template: "<div />",
|
||||
}));
|
||||
return { dashboard, wrapper };
|
||||
}
|
||||
|
||||
describe("account dashboard", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
authenticate();
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: { writeText: jest.fn(async () => {}) },
|
||||
});
|
||||
installFetch((url) => {
|
||||
if (url.includes("/api/account/overview")) return jsonResponse(overview());
|
||||
if (url.includes("/api/admin/access/requests")) {
|
||||
return jsonResponse({
|
||||
requests: [{
|
||||
username: "newuser",
|
||||
first_name: "New",
|
||||
last_name: "User",
|
||||
email: "new@example.dev",
|
||||
request_code: "newuser~abc",
|
||||
note: "please",
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({ flags: ["media", "budget"] });
|
||||
return jsonResponse({});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.restoreAllMocks();
|
||||
Object.assign(auth, {
|
||||
ready: false,
|
||||
enabled: false,
|
||||
authenticated: false,
|
||||
username: "",
|
||||
email: "",
|
||||
token: "",
|
||||
accountPasswordUrl: "",
|
||||
});
|
||||
Reflect.deleteProperty(global, "fetch");
|
||||
});
|
||||
|
||||
it("renders login-required state when unauthenticated", async () => {
|
||||
auth.ready = true;
|
||||
auth.authenticated = false;
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await nextTick();
|
||||
|
||||
expect(dashboard.mailu.status).toBe("login required");
|
||||
expect(dashboard.vaultwardenDisplayStatus.value).toBe("login required");
|
||||
expect(dashboard.admin.enabled).toBe(false);
|
||||
wrapper.unmount();
|
||||
|
||||
const view = mount(AccountView);
|
||||
await nextTick();
|
||||
expect(view.text()).toContain("Login required");
|
||||
view.unmount();
|
||||
});
|
||||
|
||||
it("reacts when auth readiness changes after mount", async () => {
|
||||
auth.ready = false;
|
||||
auth.authenticated = false;
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await nextTick();
|
||||
|
||||
auth.ready = true;
|
||||
await nextTick();
|
||||
expect(dashboard.mailu.status).toBe("login required");
|
||||
expect(dashboard.admin.requests).toEqual([]);
|
||||
|
||||
auth.authenticated = true;
|
||||
auth.username = "ada";
|
||||
auth.email = "ada@example.dev";
|
||||
auth.token = "token";
|
||||
await flushPromises();
|
||||
expect(dashboard.mailu.username).toBe("ada@bstein.dev");
|
||||
expect(dashboard.admin.enabled).toBe(true);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("loads overview, admin requests, and renders the authenticated view", async () => {
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await flushPromises();
|
||||
|
||||
expect(dashboard.mailu.username).toBe("ada@bstein.dev");
|
||||
expect(dashboard.mailu.currentPassword).toBe("mail-pass");
|
||||
expect(dashboard.wger.password).toBe("wger-pass");
|
||||
expect(dashboard.firefly.password).toBe("firefly-pass");
|
||||
expect(dashboard.vaultwardenReady.value).toBe(true);
|
||||
expect(dashboard.vaultwardenOrder.value).toBe(3);
|
||||
expect(dashboard.jellyfin.syncStatus).toBe("ok");
|
||||
expect(dashboard.onboardingUrl.value).toBe("/onboarding?code=ada");
|
||||
expect(dashboard.admin.enabled).toBe(true);
|
||||
expect(dashboard.admin.flags).toEqual(["media", "budget"]);
|
||||
expect(dashboard.hasFlag("newuser", "media")).toBe(false);
|
||||
expect(dashboard.formatName(dashboard.admin.requests[0])).toBe("New User");
|
||||
expect(dashboard.formatName({ first_name: " ", last_name: "" })).toBe("unknown");
|
||||
expect(dashboard.formatName()).toBe("unknown");
|
||||
|
||||
dashboard.toggleFlag("newuser", "media", { target: { checked: true } });
|
||||
dashboard.toggleFlag("newuser", "budget", { target: { checked: true } });
|
||||
dashboard.toggleFlag("newuser", "media", { target: { checked: false } });
|
||||
expect(dashboard.admin.selectedFlags.newuser).toEqual(["budget"]);
|
||||
wrapper.unmount();
|
||||
|
||||
const view = mount(AccountView);
|
||||
await flushPromises();
|
||||
expect(view.text()).toContain("Firefly III");
|
||||
expect(view.text()).toContain("Admin Approvals");
|
||||
expect(view.text()).toContain("newuser");
|
||||
expect(view.find("a[href='https://sso.example.dev/account/password']").exists()).toBe(true);
|
||||
view.unmount();
|
||||
});
|
||||
|
||||
it("rotates service credentials and syncs account services", async () => {
|
||||
installFetch((url) => {
|
||||
if (url.includes("/mailu/rotate")) return jsonResponse({ password: "new-mail", sync_enabled: true, sync_ok: true });
|
||||
if (url.includes("/wger/reset")) return jsonResponse({ password: "new-wger" });
|
||||
if (url.includes("/firefly/reset")) return jsonResponse({ password: "new-firefly" });
|
||||
if (url.includes("/nextcloud/mail/sync")) return jsonResponse({ ok: true });
|
||||
if (url.includes("/api/account/overview")) return jsonResponse(overview());
|
||||
if (url.includes("/api/admin/access/requests")) return jsonResponse({ requests: [] });
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({ flags: [] });
|
||||
return jsonResponse({});
|
||||
});
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await flushPromises();
|
||||
|
||||
await dashboard.rotateMailu();
|
||||
expect(dashboard.mailu.currentPassword).toBe("mail-pass");
|
||||
expect(dashboard.mailu.rotating).toBe(false);
|
||||
|
||||
await dashboard.resetWger();
|
||||
expect(dashboard.wger.password).toBe("wger-pass");
|
||||
expect(dashboard.wger.resetting).toBe(false);
|
||||
|
||||
await dashboard.resetFirefly();
|
||||
expect(dashboard.firefly.password).toBe("firefly-pass");
|
||||
expect(dashboard.firefly.resetting).toBe(false);
|
||||
|
||||
await dashboard.syncNextcloudMail();
|
||||
expect(dashboard.nextcloudMail.syncing).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("handles service action warnings and failures", async () => {
|
||||
const rotateResponses = [
|
||||
{ password: "mail-a", sync_enabled: false },
|
||||
{ password: "mail-b", sync_enabled: true, sync_ok: false, sync_error: "sync slow" },
|
||||
{ error: "ariadne unavailable" },
|
||||
];
|
||||
installFetch((url) => {
|
||||
if (url.includes("/mailu/rotate")) {
|
||||
const body = rotateResponses.shift();
|
||||
return body.error ? jsonResponse(body, 503, "Service Unavailable") : jsonResponse(body);
|
||||
}
|
||||
if (url.includes("/wger/reset")) return jsonResponse({ error: "status 502" }, 502, "Bad Gateway");
|
||||
if (url.includes("/firefly/reset")) return jsonResponse({}, 500, "Server Error");
|
||||
if (url.includes("/nextcloud/mail/sync")) return jsonResponse({ error: "status 503" }, 503, "Service Unavailable");
|
||||
if (url.includes("/api/account/overview")) return jsonResponse(overview());
|
||||
if (url.includes("/api/admin/access/requests")) return jsonResponse({ requests: [] });
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({ flags: [] });
|
||||
return jsonResponse({});
|
||||
});
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await flushPromises();
|
||||
|
||||
await dashboard.rotateMailu();
|
||||
expect(dashboard.mailu.error).toContain("Mail sync is not configured");
|
||||
await dashboard.rotateMailu();
|
||||
expect(dashboard.mailu.error).toBe("sync slow");
|
||||
await dashboard.rotateMailu();
|
||||
expect(dashboard.mailu.error).toBe("Ariadne is busy. Please try again in a moment.");
|
||||
|
||||
await dashboard.resetWger();
|
||||
expect(dashboard.wger.error).toBe("Ariadne is busy. Please try again in a moment.");
|
||||
|
||||
await dashboard.resetFirefly();
|
||||
expect(dashboard.firefly.error).toBe("status 500");
|
||||
|
||||
await dashboard.syncNextcloudMail();
|
||||
expect(dashboard.nextcloudMail.error).toContain("Refresh in a moment");
|
||||
|
||||
installFetch((url) => {
|
||||
if (url.includes("/nextcloud/mail/sync")) return jsonResponse({ error: "plain sync failed" }, 500, "Server Error");
|
||||
if (url.includes("/api/account/overview")) return jsonResponse(overview());
|
||||
if (url.includes("/api/admin/access/requests")) return jsonResponse({ requests: [] });
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({ flags: [] });
|
||||
return jsonResponse({});
|
||||
});
|
||||
await dashboard.syncNextcloudMail();
|
||||
expect(dashboard.nextcloudMail.error).toBe("plain sync failed");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("approves, denies, and copies admin/account values", async () => {
|
||||
const seen = [];
|
||||
installFetch((url, options) => {
|
||||
seen.push({ url, body: options.body });
|
||||
if (url.includes("/approve") || url.includes("/deny")) return jsonResponse({ ok: true });
|
||||
if (url.includes("/api/admin/access/requests")) return jsonResponse({ requests: [] });
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({ flags: ["media"] });
|
||||
if (url.includes("/api/account/overview")) return jsonResponse(overview());
|
||||
return jsonResponse({});
|
||||
});
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await flushPromises();
|
||||
|
||||
dashboard.admin.selectedFlags.ada = ["media"];
|
||||
dashboard.admin.notes.ada = " approve me ";
|
||||
await dashboard.approve("ada");
|
||||
expect(JSON.parse(seen.find((item) => item.url.includes("/approve")).body)).toEqual({ flags: ["media"], note: "approve me" });
|
||||
|
||||
dashboard.admin.notes.ada = " no ";
|
||||
await dashboard.deny("ada");
|
||||
expect(JSON.parse(seen.find((item) => item.url.includes("/deny")).body)).toEqual({ note: "no" });
|
||||
|
||||
await dashboard.copy("mail", "secret");
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("secret");
|
||||
expect(dashboard.copied.mail).toBe(true);
|
||||
jest.advanceTimersByTime(1500);
|
||||
expect(dashboard.copied.mail).toBe(false);
|
||||
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: { writeText: jest.fn(async () => { throw new Error("blocked"); }) },
|
||||
});
|
||||
Object.defineProperty(document, "execCommand", { configurable: true, value: jest.fn(() => true) });
|
||||
await dashboard.copy("fallback", "secret2");
|
||||
expect(document.execCommand).toHaveBeenCalledWith("copy");
|
||||
expect(dashboard.copied.fallback).toBe(true);
|
||||
jest.advanceTimersByTime(1500);
|
||||
expect(dashboard.copied.fallback).toBe(false);
|
||||
|
||||
Object.defineProperty(navigator, "clipboard", { configurable: true, value: undefined });
|
||||
await dashboard.copy("direct-fallback", "secret3");
|
||||
expect(dashboard.copied["direct-fallback"]).toBe(true);
|
||||
|
||||
Object.defineProperty(document, "execCommand", { configurable: true, value: jest.fn(() => { throw new Error("no copy"); }) });
|
||||
await dashboard.copy("ignored", "secret4");
|
||||
expect(dashboard.copied.ignored).toBeUndefined();
|
||||
|
||||
await dashboard.copy("empty", "");
|
||||
expect(dashboard.copied.empty).toBeUndefined();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("surfaces overview and admin loading failures", async () => {
|
||||
installFetch((url) => {
|
||||
if (url.includes("/api/account/overview")) return jsonResponse({ error: "overview down" }, 500, "Server Error");
|
||||
if (url.includes("/api/admin/access/requests")) return jsonResponse({}, 403, "Forbidden");
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({}, 500, "Server Error");
|
||||
return jsonResponse({});
|
||||
});
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await flushPromises();
|
||||
|
||||
expect(dashboard.mailu.status).toBe("unavailable");
|
||||
expect(dashboard.mailu.error).toContain("overview down");
|
||||
expect(dashboard.admin.enabled).toBe(false);
|
||||
expect(dashboard.admin.flags).toEqual([]);
|
||||
expect(dashboard.admin.error).toBe("status 500");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("handles admin request errors and forbidden flag lists", async () => {
|
||||
installFetch((url) => {
|
||||
if (url.includes("/api/account/overview")) return jsonResponse(overview());
|
||||
if (url.includes("/api/admin/access/requests")) return jsonResponse({}, 500, "Server Error");
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({}, 403, "Forbidden");
|
||||
return jsonResponse({});
|
||||
});
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await flushPromises();
|
||||
|
||||
expect(dashboard.admin.enabled).toBe(false);
|
||||
expect(dashboard.admin.requests).toEqual([]);
|
||||
expect(dashboard.admin.flags).toEqual([]);
|
||||
expect(dashboard.admin.error).toBe("status 500");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("reports admin action failures without leaving acting locks stuck", async () => {
|
||||
installFetch((url) => {
|
||||
if (url.includes("/approve")) return jsonResponse({ error: "approve failed" }, 500, "Server Error");
|
||||
if (url.includes("/deny")) return jsonResponse({}, 500, "Server Error");
|
||||
if (url.includes("/api/account/overview")) return jsonResponse(overview());
|
||||
if (url.includes("/api/admin/access/requests")) return jsonResponse({ requests: [] });
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({ flags: [] });
|
||||
return jsonResponse({});
|
||||
});
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await flushPromises();
|
||||
|
||||
await dashboard.approve("ada");
|
||||
expect(dashboard.admin.error).toBe("approve failed");
|
||||
expect(dashboard.admin.acting.ada).toBe(false);
|
||||
|
||||
await dashboard.deny("ada");
|
||||
expect(dashboard.admin.error).toBe("status 500");
|
||||
expect(dashboard.admin.acting.ada).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user