mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-10-31 18:21:58 +08:00
- migrate auth, manager, data, and agentic persistence to R2DBC - hard cut pagination, facade, gateway, web, and CLI contracts - remove legacy database and TSDB adapters and document validation gates BREAKING CHANGE: platform persistence now requires PostgreSQL 18 with TimescaleDB and the new reactive API contracts.
167 lines
5.8 KiB
TypeScript
167 lines
5.8 KiB
TypeScript
/*
|
|
* Copyright 2016-present the IoT DC3 original author or authors.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {createPinia, setActivePinia} from 'pinia';
|
|
|
|
import {useAuthStore} from '@/store';
|
|
import {AUTH_HEADERS} from '@/config/constant/common';
|
|
import {getStorage} from '@/utils/storageUtil';
|
|
|
|
import {seedAuthStorage, TEST_CREDENTIALS} from '../fixtures/auth';
|
|
|
|
const tokenMocks = vi.hoisted(() => ({
|
|
generateSalt: vi.fn(),
|
|
generateToken: vi.fn(),
|
|
cancelToken: vi.fn(),
|
|
checkTokenValid: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/api/token', () => tokenMocks);
|
|
|
|
const elementPlusMocks = vi.hoisted(() => {
|
|
const close = vi.fn();
|
|
const service = vi.fn(() => ({close}));
|
|
return {close, service};
|
|
});
|
|
|
|
vi.mock('element-plus', () => ({
|
|
ElLoading: {service: elementPlusMocks.service},
|
|
}));
|
|
|
|
const routerMocks = vi.hoisted(() => ({
|
|
push: vi.fn(() => Promise.resolve()),
|
|
}));
|
|
|
|
vi.mock('@/config/router', () => ({
|
|
default: {push: routerMocks.push},
|
|
}));
|
|
|
|
const notificationMocks = vi.hoisted(() => ({
|
|
failMessage: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/utils/notificationUtil', () => notificationMocks);
|
|
|
|
const loadingService = elementPlusMocks.service;
|
|
const loadingClose = elementPlusMocks.close;
|
|
const routerPush = routerMocks.push;
|
|
|
|
describe('auth store', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia());
|
|
vi.clearAllMocks();
|
|
|
|
tokenMocks.generateSalt.mockResolvedValue('salt-abc');
|
|
tokenMocks.generateToken.mockResolvedValue('token-xyz');
|
|
tokenMocks.cancelToken.mockResolvedValue(true);
|
|
});
|
|
|
|
describe('login', () => {
|
|
it('sends the raw password over HTTPS, persists token, and routes to home', async () => {
|
|
const store = useAuthStore();
|
|
await store.login({
|
|
tenant: TEST_CREDENTIALS.tenant,
|
|
name: TEST_CREDENTIALS.name,
|
|
password: TEST_CREDENTIALS.password,
|
|
});
|
|
|
|
// Salt request first, then token request with the raw password.
|
|
expect(tokenMocks.generateSalt).toHaveBeenCalledTimes(1);
|
|
expect(tokenMocks.generateSalt).toHaveBeenCalledWith({
|
|
tenant: TEST_CREDENTIALS.tenant,
|
|
name: TEST_CREDENTIALS.name,
|
|
});
|
|
|
|
expect(tokenMocks.generateToken).toHaveBeenCalledTimes(1);
|
|
const tokenPayload = tokenMocks.generateToken.mock.calls[0][0];
|
|
expect(tokenPayload.tenant).toBe(TEST_CREDENTIALS.tenant);
|
|
expect(tokenPayload.name).toBe(TEST_CREDENTIALS.name);
|
|
expect(tokenPayload.salt).toBe(TEST_CREDENTIALS.salt);
|
|
expect(tokenPayload.password).toBe(TEST_CREDENTIALS.password);
|
|
|
|
// Storage now holds tenant/login + a login flag (token is httpOnly).
|
|
expect(getStorage(AUTH_HEADERS.TENANT)).toBe(TEST_CREDENTIALS.tenant);
|
|
expect(getStorage(AUTH_HEADERS.LOGIN)).toBe(TEST_CREDENTIALS.name);
|
|
expect(getStorage(AUTH_HEADERS.AUTHENTICATED, true)).toBe(true);
|
|
|
|
// Reactive state is updated.
|
|
expect(store.tenant).toBe(TEST_CREDENTIALS.tenant);
|
|
expect(store.name).toBe(TEST_CREDENTIALS.name);
|
|
|
|
// Router pushed to home.
|
|
expect(routerPush).toHaveBeenCalledWith({name: 'home'});
|
|
|
|
// Loading was opened and closed exactly once.
|
|
expect(loadingService).toHaveBeenCalledTimes(1);
|
|
expect(loadingClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('closes the loading overlay and does not navigate when the API rejects', async () => {
|
|
tokenMocks.generateSalt.mockRejectedValueOnce(new Error('network down'));
|
|
const store = useAuthStore();
|
|
|
|
// login now catches errors internally and shows failMessage
|
|
await store.login({
|
|
tenant: TEST_CREDENTIALS.tenant,
|
|
name: TEST_CREDENTIALS.name,
|
|
password: 'x',
|
|
});
|
|
|
|
expect(loadingClose).toHaveBeenCalledTimes(1);
|
|
expect(tokenMocks.generateToken).not.toHaveBeenCalled();
|
|
expect(routerPush).not.toHaveBeenCalled();
|
|
// No partial credentials should be persisted on failure.
|
|
expect(getStorage(AUTH_HEADERS.AUTHENTICATED, true)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('logout', () => {
|
|
it('cancels the server token and clears storage when credentials exist', async () => {
|
|
const creds = seedAuthStorage();
|
|
|
|
const store = useAuthStore();
|
|
await store.logout();
|
|
|
|
expect(tokenMocks.cancelToken).toHaveBeenCalledWith({tenant: creds.tenant, name: creds.name});
|
|
expect(getStorage(AUTH_HEADERS.TENANT)).toBeUndefined();
|
|
expect(getStorage(AUTH_HEADERS.LOGIN)).toBeUndefined();
|
|
expect(getStorage(AUTH_HEADERS.AUTHENTICATED, true)).toBeUndefined();
|
|
});
|
|
|
|
it('skips cancelToken when no credentials are stored but still clears storage', async () => {
|
|
const store = useAuthStore();
|
|
await store.logout();
|
|
|
|
// Without tenant/user, the API call must not be made — otherwise we'd
|
|
// be sending an anonymous cancel that the backend would reject.
|
|
expect(tokenMocks.cancelToken).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('getters', () => {
|
|
it('exposes getTenant and getName from storage', () => {
|
|
const creds = seedAuthStorage({tenant: 'tenant-1', name: 'user-1'});
|
|
|
|
const store = useAuthStore();
|
|
|
|
expect(store.getTenant).toBe(creds.tenant);
|
|
expect(store.getName).toBe(creds.name);
|
|
});
|
|
});
|
|
});
|