237 lines
6.8 KiB
JavaScript
237 lines
6.8 KiB
JavaScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { setActivePinia, createPinia } from 'pinia';
|
|
|
|
// Mock apiError to prevent side effects (imports router, pinia, toast)
|
|
vi.mock('@/module/apiError.js', () => ({
|
|
default: vi.fn(),
|
|
}));
|
|
|
|
import axios from 'axios';
|
|
|
|
const { mockClientGet } = vi.hoisted(() => ({ mockClientGet: vi.fn() }));
|
|
vi.mock('@/api/client.js', () => ({
|
|
default: { get: mockClientGet },
|
|
}));
|
|
|
|
import { useLoginStore } from '@/stores/login.ts';
|
|
|
|
// Mock axios methods (used for signIn/refreshToken which call plain axios)
|
|
vi.spyOn(axios, 'post').mockImplementation(vi.fn());
|
|
|
|
describe('loginStore', () => {
|
|
let store;
|
|
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia());
|
|
store = useLoginStore();
|
|
store.$router = { push: vi.fn() };
|
|
vi.clearAllMocks();
|
|
// Clear cookies
|
|
document.cookie.split(';').forEach((c) => {
|
|
const name = c.split('=')[0].trim();
|
|
if (name) {
|
|
document.cookie = name + '=; Max-Age=-99999999; path=/';
|
|
}
|
|
});
|
|
});
|
|
|
|
it('has correct default state', () => {
|
|
expect(store.auth.grant_type).toBe('password');
|
|
expect(store.auth.username).toBe('');
|
|
expect(store.isLoggedIn).toBe(false);
|
|
expect(store.isInvalid).toBe(false);
|
|
});
|
|
|
|
describe('signIn', () => {
|
|
it('stores token and navigates on success', async () => {
|
|
axios.post.mockResolvedValue({
|
|
data: {
|
|
access_token: 'test-access-token',
|
|
refresh_token: 'test-refresh-token',
|
|
},
|
|
});
|
|
|
|
await store.signIn();
|
|
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
'/api/oauth/token',
|
|
store.auth,
|
|
expect.objectContaining({
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
}),
|
|
);
|
|
expect(store.isLoggedIn).toBe(true);
|
|
// Verify token cookie was set with Secure flag
|
|
// (jsdom drops Secure cookies, so spy on setter)
|
|
const cookieSetter = vi.spyOn(document, 'cookie', 'set');
|
|
vi.clearAllMocks();
|
|
axios.post.mockResolvedValue({
|
|
data: {
|
|
access_token: 'test-access-token',
|
|
refresh_token: 'test-refresh-token',
|
|
},
|
|
});
|
|
await store.signIn();
|
|
const tokenCall = cookieSetter.mock.calls.find(
|
|
(c) => c[0].includes('luciaToken='),
|
|
);
|
|
expect(tokenCall).toBeDefined();
|
|
expect(tokenCall[0]).toContain('Secure');
|
|
cookieSetter.mockRestore();
|
|
expect(store.$router.push).toHaveBeenCalledWith('/files');
|
|
});
|
|
|
|
it('redirects to remembered URL when set', async () => {
|
|
axios.post.mockResolvedValue({
|
|
data: {
|
|
access_token: 'token',
|
|
refresh_token: 'refresh',
|
|
},
|
|
});
|
|
// btoa('/dashboard') = 'L2Rhc2hib2FyZA=='
|
|
store.rememberedReturnToUrl = btoa('/dashboard');
|
|
|
|
// Mock window.location.href setter
|
|
const originalLocation = window.location;
|
|
delete window.location;
|
|
window.location = { href: '' };
|
|
|
|
await store.signIn();
|
|
|
|
expect(window.location.href).toBe('/dashboard');
|
|
window.location = originalLocation;
|
|
});
|
|
|
|
it('sets isInvalid on error', async () => {
|
|
axios.post.mockRejectedValue(new Error('401'));
|
|
|
|
await store.signIn();
|
|
|
|
expect(store.isInvalid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('logOut', () => {
|
|
it('clears auth state and navigates to login', () => {
|
|
store.isLoggedIn = true;
|
|
store.logOut();
|
|
|
|
expect(store.isLoggedIn).toBe(false);
|
|
expect(store.$router.push).toHaveBeenCalledWith('/login');
|
|
});
|
|
});
|
|
|
|
describe('getUserData', () => {
|
|
it('stores user data on success', async () => {
|
|
mockClientGet.mockResolvedValue({
|
|
data: { username: 'testuser', name: 'Test User' },
|
|
});
|
|
|
|
await store.getUserData();
|
|
|
|
expect(mockClientGet).toHaveBeenCalledWith('/api/my-account');
|
|
expect(store.userData).toEqual({
|
|
username: 'testuser',
|
|
name: 'Test User',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('checkLogin', () => {
|
|
it('does not redirect on success', async () => {
|
|
mockClientGet.mockResolvedValue({ data: {} });
|
|
|
|
await store.checkLogin();
|
|
|
|
expect(store.$router.push).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('redirects to login on error', async () => {
|
|
mockClientGet.mockRejectedValue(new Error('401'));
|
|
|
|
await store.checkLogin();
|
|
|
|
expect(store.$router.push).toHaveBeenCalledWith('/login');
|
|
});
|
|
});
|
|
|
|
it('setRememberedReturnToUrl stores URL', () => {
|
|
store.setRememberedReturnToUrl('abc');
|
|
expect(store.rememberedReturnToUrl).toBe('abc');
|
|
});
|
|
|
|
it('setIsLoggedIn sets boolean', () => {
|
|
store.setIsLoggedIn(true);
|
|
expect(store.isLoggedIn).toBe(true);
|
|
});
|
|
|
|
describe('refreshToken', () => {
|
|
it('sends request with correct config and updates tokens on success', async () => {
|
|
document.cookie = 'luciaRefreshToken=old-refresh-token';
|
|
|
|
axios.post.mockResolvedValue({
|
|
status: 200,
|
|
data: {
|
|
access_token: 'new-access-token',
|
|
refresh_token: 'new-refresh-token',
|
|
},
|
|
});
|
|
|
|
await store.refreshToken();
|
|
|
|
// Should call with content-type header (config must be defined)
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
'/api/oauth/token',
|
|
expect.objectContaining({
|
|
grant_type: 'refresh_token',
|
|
refresh_token: 'old-refresh-token',
|
|
}),
|
|
expect.objectContaining({
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
}),
|
|
);
|
|
|
|
// Verify cookies were set with Secure flag
|
|
const cookieSetter = vi.spyOn(document, 'cookie', 'set');
|
|
vi.clearAllMocks();
|
|
document.cookie = 'luciaRefreshToken=old-refresh-token';
|
|
axios.post.mockResolvedValue({
|
|
status: 200,
|
|
data: {
|
|
access_token: 'new-access-token',
|
|
refresh_token: 'new-refresh-token',
|
|
},
|
|
});
|
|
await store.refreshToken();
|
|
const tokenCall = cookieSetter.mock.calls.find(
|
|
(c) => c[0].includes('luciaToken='),
|
|
);
|
|
expect(tokenCall).toBeDefined();
|
|
expect(tokenCall[0]).toContain('Secure');
|
|
cookieSetter.mockRestore();
|
|
});
|
|
|
|
it('redirects to login and re-throws on failure', async () => {
|
|
document.cookie = 'luciaRefreshToken=old-refresh-token';
|
|
axios.post.mockRejectedValue(new Error('401'));
|
|
|
|
await expect(store.refreshToken()).rejects.toThrow('401');
|
|
|
|
expect(store.$router.push).toHaveBeenCalledWith('/login');
|
|
});
|
|
});
|
|
|
|
describe('expired', () => {
|
|
it('is approximately 6 months in the future', () => {
|
|
const now = new Date();
|
|
const sixMonthsLater = new Date(now);
|
|
sixMonthsLater.setMonth(sixMonthsLater.getMonth() + 6);
|
|
|
|
const expiredDate = new Date(store.expired);
|
|
// Allow 1 day tolerance
|
|
const diffMs = Math.abs(expiredDate.getTime() - sixMonthsLater.getTime());
|
|
expect(diffMs).toBeLessThan(24 * 60 * 60 * 1000);
|
|
});
|
|
});
|
|
});
|