168 lines
5.5 KiB
TypeScript
168 lines
5.5 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
|
/**
|
|
* Unit tests for auth.client.ts plugin
|
|
*
|
|
* This plugin is responsible for initializing the auth state when the app mounts.
|
|
* It calls useAuth().initAuth() which:
|
|
* 1. Syncs user from Pocketbase authStore (session restoration)
|
|
* 2. Sets up cross-tab sync listener via pb.authStore.onChange()
|
|
*
|
|
* Tests written FIRST (TDD Red phase) - Implementation does not exist yet.
|
|
* Expected: ALL TESTS WILL FAIL until T004 is implemented.
|
|
*
|
|
* Story Mapping:
|
|
* - US4 (Session Persistence): Plugin enables session restoration on page load
|
|
* - US5 (Cross-Tab Sync): Plugin sets up onChange listener for cross-tab synchronization
|
|
*/
|
|
|
|
// Mock useAuth composable
|
|
const mockInitAuth = vi.fn();
|
|
const mockUseAuth = vi.fn(() => ({
|
|
initAuth: mockInitAuth,
|
|
user: { value: null },
|
|
isAuthenticated: { value: false },
|
|
loading: { value: false },
|
|
error: { value: null },
|
|
login: vi.fn(),
|
|
logout: vi.fn(),
|
|
handleOAuthCallback: vi.fn(),
|
|
refreshAuth: vi.fn(),
|
|
authProviders: vi.fn(),
|
|
}));
|
|
|
|
// Mock the useAuth composable
|
|
vi.mock('../../composables/useAuth', () => ({
|
|
useAuth: mockUseAuth,
|
|
}));
|
|
|
|
// Mock defineNuxtPlugin
|
|
const mockDefineNuxtPlugin = vi.fn((callback: Function) => {
|
|
// Store the callback so we can invoke it in tests
|
|
return { callback };
|
|
});
|
|
|
|
vi.mock('#app', () => ({
|
|
defineNuxtPlugin: mockDefineNuxtPlugin,
|
|
}));
|
|
|
|
describe('auth.client plugin', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('Plugin Initialization', () => {
|
|
it('should call initAuth() exactly once when plugin executes', async () => {
|
|
// Import the plugin (this will execute it)
|
|
const plugin = await import('../auth.client');
|
|
|
|
// The plugin should have called defineNuxtPlugin
|
|
expect(mockDefineNuxtPlugin).toHaveBeenCalledTimes(1);
|
|
|
|
// Get the plugin callback
|
|
const pluginCallback = mockDefineNuxtPlugin.mock.results[0]?.value?.callback;
|
|
expect(pluginCallback).toBeDefined();
|
|
expect(typeof pluginCallback).toBe('function');
|
|
|
|
// Execute the plugin callback
|
|
pluginCallback();
|
|
|
|
// Verify useAuth was called
|
|
expect(mockUseAuth).toHaveBeenCalledTimes(1);
|
|
|
|
// Verify initAuth was called exactly once
|
|
expect(mockInitAuth).toHaveBeenCalledTimes(1);
|
|
expect(mockInitAuth).toHaveBeenCalledWith();
|
|
});
|
|
|
|
it('should call useAuth composable to get auth methods', async () => {
|
|
// Import the plugin
|
|
const plugin = await import('../auth.client');
|
|
|
|
// Get and execute the plugin callback
|
|
const pluginCallback = mockDefineNuxtPlugin.mock.results[0]?.value?.callback;
|
|
pluginCallback();
|
|
|
|
// Verify useAuth was called
|
|
expect(mockUseAuth).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Plugin Execution Order', () => {
|
|
it('should execute synchronously (not async)', async () => {
|
|
// Import the plugin
|
|
const plugin = await import('../auth.client');
|
|
|
|
// Get the plugin callback
|
|
const pluginCallback = mockDefineNuxtPlugin.mock.results[0]?.value?.callback;
|
|
|
|
// The callback should not return a Promise
|
|
const result = pluginCallback();
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should not call any other auth methods besides initAuth', async () => {
|
|
// Create a mock with spy methods
|
|
const spyLogin = vi.fn();
|
|
const spyLogout = vi.fn();
|
|
const spyHandleOAuthCallback = vi.fn();
|
|
const spyRefreshAuth = vi.fn();
|
|
|
|
mockUseAuth.mockReturnValue({
|
|
initAuth: mockInitAuth,
|
|
user: { value: null },
|
|
isAuthenticated: { value: false },
|
|
loading: { value: false },
|
|
error: { value: null },
|
|
login: spyLogin,
|
|
logout: spyLogout,
|
|
handleOAuthCallback: spyHandleOAuthCallback,
|
|
refreshAuth: spyRefreshAuth,
|
|
authProviders: vi.fn(),
|
|
});
|
|
|
|
// Import and execute the plugin
|
|
const plugin = await import('../auth.client');
|
|
const pluginCallback = mockDefineNuxtPlugin.mock.results[0]?.value?.callback;
|
|
pluginCallback();
|
|
|
|
// Verify only initAuth was called
|
|
expect(mockInitAuth).toHaveBeenCalled();
|
|
expect(spyLogin).not.toHaveBeenCalled();
|
|
expect(spyLogout).not.toHaveBeenCalled();
|
|
expect(spyHandleOAuthCallback).not.toHaveBeenCalled();
|
|
expect(spyRefreshAuth).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Client-Side Only Execution', () => {
|
|
it('should be a client-side plugin (file named auth.client.ts)', async () => {
|
|
// This test verifies the naming convention
|
|
// Plugin files ending in .client.ts are automatically client-side only in Nuxt
|
|
// We can't directly test this in unit tests, but we document the requirement
|
|
|
|
// The file MUST be named auth.client.ts (not auth.ts)
|
|
// This ensures it only runs in the browser, not during SSR
|
|
|
|
const filename = 'auth.client.ts';
|
|
expect(filename).toMatch(/\.client\.ts$/);
|
|
});
|
|
});
|
|
|
|
describe('Integration with useAuth', () => {
|
|
it('should enable session restoration via initAuth', async () => {
|
|
// This test documents the expected behavior:
|
|
// When initAuth() is called, it should:
|
|
// 1. Sync user from pb.authStore.record
|
|
// 2. Set up onChange listener for cross-tab sync
|
|
|
|
const plugin = await import('../auth.client');
|
|
const pluginCallback = mockDefineNuxtPlugin.mock.results[0]?.value?.callback;
|
|
pluginCallback();
|
|
|
|
// Verify initAuth was called (the actual restoration logic is tested in useAuth.test.ts)
|
|
expect(mockInitAuth).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|