/** * Auth store tests * * TRACES: UR-009, UR-012 | IR-009, IR-014 */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { auth, isAuthenticated, currentUser, authError } from "./auth"; import { get } from "svelte/store"; vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(async (command: string, args?: any) => { if (command === "auth_initialize") { return null; // No stored session } if (command === "storage_get_security_status") { return { usingKeyring: true, storageType: "keyring", }; } if (command === "auth_connect_to_server") { return { name: "My Server", version: "10.8.0", id: "server-123", normalizedUrl: "http://server.local:8096", }; } if (command === "auth_login") { return { user: { id: "user-123", name: "testuser", serverId: "server-123", }, serverId: "server-123", accessToken: "token-abc123", }; } if (command === "auth_get_session") { return null; } if (command === "auth_start_verification") { return undefined; } if (command === "auth_logout") { return undefined; } if (command === "storage_save_server") { return undefined; } if (command === "storage_save_user") { return undefined; } if (command === "storage_set_active_user") { return undefined; } if (command === "auth_set_session") { return undefined; } if (command === "player_configure_jellyfin") { return undefined; } if (command === "player_disable_jellyfin") { return undefined; } return null; }), })); vi.mock("@tauri-apps/api/event", () => ({ listen: vi.fn(async (event: string) => { return () => {}; // Return empty unlisten function }), })); vi.mock("$lib/services/deviceId", () => ({ getDeviceId: vi.fn(async () => "device-id-123"), clearCache: vi.fn(), })); vi.mock("$lib/stores/connectivity", () => ({ connectivity: { startMonitoring: vi.fn(async () => undefined), }, })); vi.mock("$lib/api/repository-client", () => ({ RepositoryClient: class { async create() {} async destroy() {} }, })); describe("auth store", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("initial state", () => { it("should start with unauthenticated state", () => { const state = get(auth); expect(state.isAuthenticated).toBe(false); expect(state.user).toBeNull(); expect(state.serverUrl).toBeNull(); }); it("should start with loading true", () => { const state = get(auth); expect(state.isLoading).toBe(true); }); it("should have error as null initially", () => { const state = get(auth); expect(state.error).toBeNull(); }); }); describe("auth state structure", () => { it("should have isAuthenticated boolean", () => { const state = get(auth); expect(typeof state.isAuthenticated).toBe("boolean"); }); it("should have isLoading boolean", () => { const state = get(auth); expect(typeof state.isLoading).toBe("boolean"); }); it("should have user or null", () => { const state = get(auth); expect(state.user === null || typeof state.user === "object").toBe(true); }); it("should have serverUrl or null", () => { const state = get(auth); expect(state.serverUrl === null || typeof state.serverUrl === "string").toBe(true); }); it("should have error or null", () => { const state = get(auth); expect(state.error === null || typeof state.error === "string").toBe(true); }); it("should have needsReauth boolean", () => { const state = get(auth); expect(typeof state.needsReauth).toBe("boolean"); }); it("should have sessionVerified boolean", () => { const state = get(auth); expect(typeof state.sessionVerified).toBe("boolean"); }); }); describe("derived stores", () => { it("should provide isAuthenticated derived store", () => { const authenticated = get(isAuthenticated); expect(typeof authenticated).toBe("boolean"); }); it("should provide currentUser derived store", () => { const user = get(currentUser); expect(user === null || typeof user === "object").toBe(true); }); it("should provide authError derived store", () => { const error = get(authError); expect(error === null || typeof error === "string").toBe(true); }); }); describe("clearError", () => { it("should clear error state", async () => { // Get initial state and verify error is null const initialState = get(auth); expect(initialState.error).toBeNull(); // Call clearError auth.clearError(); // Verify error is still null (no change) const afterClear = get(auth); expect(afterClear.error).toBeNull(); }); }); describe("getUserId", () => { it("should return user ID from state", () => { const userId = auth.getUserId(); expect(userId === null || typeof userId === "string").toBe(true); }); }); describe("getServerUrl", () => { it("should return server URL from state", () => { const serverUrl = auth.getServerUrl(); expect(serverUrl === null || typeof serverUrl === "string").toBe(true); }); }); describe("subscriptions", () => { it("should allow subscriptions to auth changes", () => { const states: any[] = []; const unsubscribe = auth.subscribe((state) => { states.push(state); }); expect(states.length).toBeGreaterThan(0); unsubscribe(); }); it("should notify multiple subscribers", () => { const states1: any[] = []; const states2: any[] = []; const unsub1 = auth.subscribe((state) => states1.push(state)); const unsub2 = auth.subscribe((state) => states2.push(state)); expect(states1.length).toBeGreaterThan(0); expect(states2.length).toBeGreaterThan(0); unsub1(); unsub2(); }); }); describe("connectToServer", () => { it("should invoke correct backend command", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); try { await auth.connectToServer("http://server.local:8096"); } catch (e) { // Expected - might fail due to mocking } const call = invokeSpy.mock.calls.find((c) => c[0] === "auth_connect_to_server"); expect(call).toBeDefined(); expect(call![1]).toHaveProperty("serverUrl"); }); it("should return normalized server info", async () => { const serverInfo = await auth.connectToServer("http://server.local:8096"); expect(serverInfo).toHaveProperty("name"); expect(serverInfo).toHaveProperty("version"); expect(serverInfo).toHaveProperty("id"); expect(serverInfo).toHaveProperty("normalizedUrl"); }); }); describe("login", () => { it("should accept username, password, serverUrl, and serverName", async () => { try { await auth.login("testuser", "password123", "http://server.local:8096", "My Server"); } catch (e) { // Expected - RepositoryClient is mocked } const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); const loginCall = invokeSpy.mock.calls.find((c) => c[0] === "auth_login"); expect(loginCall).toBeDefined(); expect(loginCall![1]).toHaveProperty("username", "testuser"); expect(loginCall![1]).toHaveProperty("password", "password123"); }); it("should invoke auth_login backend command", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); try { await auth.login("user", "pass", "http://localhost", "Server"); } catch (e) { // Expected - mocking limitations } expect(invokeSpy).toHaveBeenCalledWith( "auth_login", expect.objectContaining({ username: "user", password: "pass", }), ); }); }); describe("logout", () => { it("should clear authentication state", async () => { await auth.logout(); const state = get(auth); expect(state.isAuthenticated).toBe(false); expect(state.user).toBeNull(); }); it("should clear server URL", async () => { await auth.logout(); const state = get(auth); expect(state.serverUrl).toBeNull(); }); it("should clear server name", async () => { await auth.logout(); const state = get(auth); expect(state.serverName).toBeNull(); }); it("should invoke backend logout command", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await auth.logout(); // Either auth_get_session or auth_logout should be called const callNames = invokeSpy.mock.calls.map((c) => c[0]); expect( callNames.some((name) => ["auth_get_session", "player_disable_jellyfin"].includes(name)), ).toBe(true); }); }); describe("getCurrentSession", () => { it("should return session or null", async () => { const session = await auth.getCurrentSession(); expect(session === null || typeof session === "object").toBe(true); }); it("should invoke backend command", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await auth.getCurrentSession(); const call = invokeSpy.mock.calls.find((c) => c[0] === "auth_get_session"); expect(call).toBeDefined(); }); }); describe("initialize", () => { it("should call initialize without throwing", async () => { // This is already called during store creation // Just verify the store is in a valid state const state = get(auth); expect(state).toBeDefined(); expect(typeof state.isAuthenticated).toBe("boolean"); }); }); });