Files
ctms/frontend/src/runtime/savedLoginCredentials.test.ts
T

138 lines
4.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DESKTOP_SERVER_URL_KEY } from "./desktopServerConfig";
import {
clearLoginCredential,
getSavedLoginCredential,
saveLoginCredential,
} from "./savedLoginCredentials";
const invokeMock = vi.hoisted(() => vi.fn());
vi.mock("@tauri-apps/api/core", () => ({
invoke: invokeMock,
}));
const SERVER_ORIGIN = "https://ctms.example.com/";
const createStorage = (): Storage => {
const data = new Map<string, string>();
return {
get length() {
return data.size;
},
clear: () => data.clear(),
getItem: (key) => data.get(key) ?? null,
key: (index) => Array.from(data.keys())[index] ?? null,
removeItem: (key) => data.delete(key),
setItem: (key, value) => {
data.set(key, String(value));
},
};
};
describe("saved login credentials", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-08T00:00:00.000Z"));
Object.defineProperty(window, "localStorage", { value: createStorage(), configurable: true });
localStorage.clear();
invokeMock.mockReset();
Reflect.deleteProperty(window, "__TAURI_INTERNALS__");
Reflect.deleteProperty(window, "PasswordCredential");
Reflect.deleteProperty(navigator, "credentials");
});
afterEach(() => {
vi.useRealTimers();
localStorage.clear();
Reflect.deleteProperty(window, "__TAURI_INTERNALS__");
Reflect.deleteProperty(window, "PasswordCredential");
Reflect.deleteProperty(navigator, "credentials");
});
it("stores desktop remembered passwords in the system credential store", async () => {
Object.defineProperty(window, "__TAURI_INTERNALS__", { value: {}, configurable: true });
localStorage.setItem(DESKTOP_SERVER_URL_KEY, SERVER_ORIGIN);
let stored = "";
invokeMock.mockImplementation(async (command: string, args: any) => {
if (command === "login_credential_set") {
stored = args.credential;
return undefined;
}
if (command === "login_credential_get") return stored;
if (command === "login_credential_delete") {
stored = "";
return undefined;
}
return undefined;
});
await expect(saveLoginCredential("Admin@Example.com", "secret-password")).resolves.toBe(true);
expect(invokeMock).toHaveBeenCalledWith("login_credential_set", {
serverOrigin: SERVER_ORIGIN,
credential: expect.any(String),
});
expect(JSON.parse(stored)).toMatchObject({
version: 1,
email: "admin@example.com",
password: "secret-password",
savedAt: Date.now(),
});
await expect(getSavedLoginCredential()).resolves.toEqual({
email: "admin@example.com",
password: "secret-password",
});
await expect(clearLoginCredential()).resolves.toBe(true);
expect(invokeMock).toHaveBeenCalledWith("login_credential_delete", { serverOrigin: SERVER_ORIGIN });
});
it("deletes malformed desktop remembered credentials", async () => {
Object.defineProperty(window, "__TAURI_INTERNALS__", { value: {}, configurable: true });
localStorage.setItem(DESKTOP_SERVER_URL_KEY, SERVER_ORIGIN);
invokeMock.mockImplementation(async (command: string) => {
if (command === "login_credential_get") return JSON.stringify({ password: "missing-email" });
return undefined;
});
await expect(getSavedLoginCredential()).resolves.toBeNull();
expect(invokeMock).toHaveBeenCalledWith("login_credential_delete", { serverOrigin: SERVER_ORIGIN });
});
it("uses browser credential management without writing passwords to localStorage", async () => {
const storeMock = vi.fn();
const preventSilentAccessMock = vi.fn();
Object.defineProperty(window, "PasswordCredential", {
configurable: true,
value: class {
id: string;
password: string;
constructor(data: { id: string; password: string }) {
this.id = data.id;
this.password = data.password;
}
},
});
Object.defineProperty(navigator, "credentials", {
configurable: true,
value: {
get: vi.fn(async () => ({ id: "Browser@Example.com", password: "browser-secret" })),
store: storeMock,
preventSilentAccess: preventSilentAccessMock,
},
});
await expect(getSavedLoginCredential()).resolves.toEqual({
email: "browser@example.com",
password: "browser-secret",
});
await expect(saveLoginCredential("Browser@Example.com", "browser-secret")).resolves.toBe(true);
await expect(clearLoginCredential()).resolves.toBe(true);
expect(storeMock).toHaveBeenCalledTimes(1);
expect(preventSilentAccessMock).toHaveBeenCalledTimes(1);
expect(JSON.stringify(localStorage)).not.toContain("browser-secret");
});
});