Files
pnoker eae86b58fc refactor(storage)!: unify platform persistence on reactive postgresql
- 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.
2026-09-01 22:03:16 +08:00

111 lines
4.1 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 type {AxiosAdapter, InternalAxiosRequestConfig} from 'axios';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import request from '@/config/axios';
import {AUTH_HEADERS} from '@/config/constant/common';
import {getStorage, setStorage} from '@/utils/storageUtil';
const notificationSpies = vi.hoisted(() => ({
failMessage: vi.fn(),
warnMessage: vi.fn(),
}));
vi.mock('@/utils/notificationUtil', () => notificationSpies);
const routerMocks = vi.hoisted(() => ({
push: vi.fn(() => Promise.resolve()),
}));
vi.mock('@/config/router', () => ({
default: {push: routerMocks.push},
}));
const responseOf = (config: InternalAxiosRequestConfig, status: number, data: unknown) => ({
data,
status,
statusText: String(status),
headers: {},
config,
request: {},
});
describe('axios request instance', () => {
beforeEach(() => {
notificationSpies.failMessage.mockClear();
notificationSpies.warnMessage.mockClear();
});
it('injects auth headers and preserves string identifiers in JSON responses', async () => {
setStorage(AUTH_HEADERS.TENANT, 'default');
setStorage(AUTH_HEADERS.LOGIN, 'dc3');
const adapter: AxiosAdapter = vi.fn(async (config) => {
expect(config.headers.get(AUTH_HEADERS.TENANT)).toBe('default');
expect(config.headers.get(AUTH_HEADERS.LOGIN)).toBe('dc3');
// Token is NOT injected — it travels in an httpOnly cookie.
expect(config.headers.get(AUTH_HEADERS.TOKEN)).toBeUndefined();
return responseOf(config, 200, '{"id":"9007199254740993"}');
});
const response = await request({
url: 'api/v3/manager/device/select_by_id',
method: 'get',
params: {id: 1},
adapter,
});
expect(response).toEqual({id: '9007199254740993'});
});
it('removes auth keys and routes to login on unauthorized responses', async () => {
setStorage(AUTH_HEADERS.TENANT, 'default');
setStorage(AUTH_HEADERS.LOGIN, 'dc3');
setStorage(AUTH_HEADERS.AUTHENTICATED, true, true);
const problem = {type: 'about:blank', title: 'Unauthorized', status: 401, code: 'AUTH_UNAUTHORIZED'};
const adapter: AxiosAdapter = async (config) => responseOf(config, 401, problem);
await expect(request({url: 'api/v3/auth/token/check', method: 'post', adapter})).rejects.toBe(problem);
expect(notificationSpies.warnMessage).toHaveBeenCalledTimes(1);
// Only auth keys are removed — not the entire localStorage
expect(getStorage(AUTH_HEADERS.TENANT)).toBeUndefined();
expect(getStorage(AUTH_HEADERS.LOGIN)).toBeUndefined();
expect(getStorage(AUTH_HEADERS.AUTHENTICATED, true)).toBeUndefined();
// Routes via router.push instead of raw hash manipulation
expect(routerMocks.push).toHaveBeenCalledWith({name: 'login'});
});
it('rejects non-ok business responses and surfaces the server payload', async () => {
const payload = {type: 'about:blank', title: 'Bad Request', status: 400, code: 'VALIDATION_FAILED', detail: 'business failed'};
// Use status 400 — non-ok, non-401, non-5xx hits the failMessage branch
const adapter: AxiosAdapter = async (config) => responseOf(config, 400, payload);
await expect(request({url: 'api/v3/data/dashboard/stats/today', method: 'get', adapter})).rejects.toBe(payload);
expect(notificationSpies.failMessage).toHaveBeenCalledWith(
'API request error. Please contact the system administrator.',
'VALIDATION_FAILED',
payload
);
});
});