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: "
", })); 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(); }); });