Files
jellytau/src/lib/stores/auth.test.ts
T
dtourolle ad48d89dfe chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check`
reported 199 unformatted files and ran in no workflow and in no git hook,
so .prettierrc (printWidth 100, trailing commas) described an intention
rather than the tree.

This is the one-time sweep that makes the check gateable. Whitespace and
token-reflow only -- no behavioural change: `bun run check` reports 0
errors and all 1053 frontend tests pass before and after.

Kept out of every other commit on purpose. A 199-file diff mixed with
real changes is unreviewable, and the next commit turns format:check
into a hard CI gate so this cannot silently accumulate again.
2026-08-21 17:41:44 +02:00

356 lines
9.9 KiB
TypeScript

/**
* 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");
});
});
});