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.wolf.status).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.wolf.status).toBe("degraded"); 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("Wolf"); 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("loads and controls Wolf account state", async () => { const seen = []; installFetch((url, options = {}) => { seen.push({ url, body: options.body || "" }); if (url.includes("/api/account/wolf/firewall/unlock")) return jsonResponse({ success: true }); if (url.includes("/api/account/wolf/pairing/submit-pin")) return jsonResponse({ success: true }); if (url.includes("/api/account/wolf/game-mode/start")) return jsonResponse({ status: "active" }); if (url.includes("/api/account/wolf/game-mode/stop")) return jsonResponse({ status: "idle" }); if (url.includes("/api/account/wolf/admin/firewall/unlock")) return jsonResponse({ success: true }); if (url.includes("/api/account/wolf")) { return jsonResponse({ can_control_gpu: true, moonlight: { host: "moonlight.bstein.dev", source_ip: "1.2.3.4", unlock_ttl_seconds: 28800 }, gpu: { priority: "ai", game_mode: { status: "idle", active: false } }, wolf: { api_enabled: true, clients: [{ name: "Desktop" }], pending_pair_requests: [{ name: "Laptop", pair_secret: "secret-1" }], sessions: [], apps: [{ name: "Steam" }], }, firewall: { active_unlocks: [{ ip: "1.2.3.4" }] }, }); } 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(); expect(dashboard.wolf.status).toBe("ready"); expect(dashboard.wolf.canControlGpu).toBe(true); expect(dashboard.wolf.clients[0].name).toBe("Desktop"); expect(dashboard.wolf.pendingPairRequests[0].pair_secret).toBe("secret-1"); await dashboard.unlockWolf(); dashboard.wolf.pinInputs["secret-1"] = "1234"; await dashboard.pairWolf("secret-1"); dashboard.wolf.selectedGame = "arc-raiders"; dashboard.wolf.note = "now"; await dashboard.setWolfGameMode("start"); await dashboard.setWolfGameMode("stop"); dashboard.wolf.manualIp = "5.6.7.8"; dashboard.wolf.manualUser = "olya"; await dashboard.adminUnlockWolfIp(); expect(JSON.parse(seen.find((item) => item.url.includes("/pairing/submit-pin")).body)).toEqual({ pair_secret: "secret-1", pin: "1234", }); expect(JSON.parse(seen.find((item) => item.url.includes("/game-mode/start")).body)).toEqual({ game: "arc-raiders", note: "now", }); expect(JSON.parse(seen.find((item) => item.url.includes("/admin/firewall/unlock")).body)).toEqual({ ip: "5.6.7.8", target_user: "olya", }); 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(); }); });