mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
fix: prevent expired-token login refresh loop
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const ts = require('typescript');
|
||||
const axios = require('axios');
|
||||
|
||||
function fixture(pathname = '/login', baseUrl = '/') {
|
||||
const storage = new Map([['token', 'expired-token']]);
|
||||
const pushes = [];
|
||||
const history = { location: { pathname }, push: (p) => pushes.push(p) };
|
||||
const config = { authKey: 'token', baseUrl };
|
||||
const mocks = {
|
||||
axios,
|
||||
'react-intl-universal': { get: (s) => s },
|
||||
antd: {
|
||||
message: { config() {}, error() {} },
|
||||
notification: { error() {} },
|
||||
},
|
||||
'./config': config,
|
||||
'@umijs/max': { history },
|
||||
'./httpError': { getErrorDetails: () => [] },
|
||||
};
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '../../src/utils/http.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
const { outputText } = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
jsx: ts.JsxEmit.React,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
});
|
||||
const module = { exports: {} };
|
||||
new Function('require', 'module', 'exports', 'localStorage', outputText)(
|
||||
(name) => {
|
||||
assert.ok(Object.hasOwn(mocks, name), name);
|
||||
return mocks[name];
|
||||
},
|
||||
module,
|
||||
module.exports,
|
||||
{
|
||||
getItem: (k) => storage.get(k) ?? null,
|
||||
removeItem: (k) => storage.delete(k),
|
||||
},
|
||||
);
|
||||
return { request: module.exports.request, storage, pushes };
|
||||
}
|
||||
|
||||
for (const pathname of ['/login', '/dashboard']) {
|
||||
test(`401 clears expired credentials on ${pathname}`, async () => {
|
||||
const { request, storage, pushes } = fixture(pathname);
|
||||
await assert.rejects(
|
||||
request.get('/api/user', {
|
||||
adapter: async (config) => {
|
||||
throw new axios.AxiosError(
|
||||
'Unauthorized',
|
||||
'ERR_BAD_REQUEST',
|
||||
config,
|
||||
null,
|
||||
{ status: 401, data: { message: 'expired' }, config },
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.equal(storage.has('token'), false);
|
||||
assert.deepEqual(pushes, pathname === '/login' ? [] : ['/login']);
|
||||
});
|
||||
}
|
||||
for (const baseUrl of ['/', '/panel/']) {
|
||||
test(`health is anonymous while protected requests keep credentials (${baseUrl})`, async () => {
|
||||
const { request } = fixture('/login', baseUrl);
|
||||
const headers = [];
|
||||
const adapter = async (config) => {
|
||||
headers.push(config.headers.get('Authorization'));
|
||||
return { status: 200, data: { code: 200 }, config };
|
||||
};
|
||||
await request.get(`${baseUrl}api/health`, { adapter });
|
||||
await request.get(`${baseUrl}api/user`, { adapter });
|
||||
assert.deepEqual(headers, [undefined, 'Bearer expired-token']);
|
||||
});
|
||||
}
|
||||
|
||||
function healthFixture(get) {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '../../src/layouts/index.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
const ast = ts.createSourceFile(
|
||||
'index.tsx',
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TSX,
|
||||
);
|
||||
let initializer;
|
||||
function visit(node) {
|
||||
if (
|
||||
ts.isVariableDeclaration(node) &&
|
||||
node.name.getText(ast) === 'getHealthStatus'
|
||||
)
|
||||
initializer = node.initializer;
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(ast);
|
||||
assert.ok(initializer, 'layout health bootstrap exists');
|
||||
const { outputText } = ts.transpileModule(
|
||||
`const bootstrap = ${initializer.getText(ast)};`,
|
||||
{
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2020 },
|
||||
},
|
||||
);
|
||||
const events = [];
|
||||
const bootstrap = new Function(
|
||||
'request',
|
||||
'config',
|
||||
'history',
|
||||
'window',
|
||||
'getSystemInfo',
|
||||
'setInitLoading',
|
||||
`${outputText}; return bootstrap;`,
|
||||
)(
|
||||
{ get },
|
||||
{ apiPrefix: '/api/' },
|
||||
{ push: (p) => events.push(p) },
|
||||
{ location: { reload: () => events.push('reload') } },
|
||||
() => events.push('system'),
|
||||
(value) => events.push(['loading', value]),
|
||||
);
|
||||
return { bootstrap, events };
|
||||
}
|
||||
for (const status of [401, 503, undefined]) {
|
||||
test(`health failure ${
|
||||
status ?? 'network'
|
||||
} settles without reloading`, async () => {
|
||||
const { bootstrap, events } = healthFixture(async () => {
|
||||
throw status ? { response: { status } } : new Error('Network Error');
|
||||
});
|
||||
bootstrap();
|
||||
await new Promise(setImmediate);
|
||||
assert.deepEqual(events, ['/error', ['loading', false]]);
|
||||
});
|
||||
}
|
||||
test('healthy bootstrap continues loading system information', async () => {
|
||||
const { bootstrap, events } = healthFixture(async () => ({
|
||||
data: { status: 'ok' },
|
||||
}));
|
||||
bootstrap();
|
||||
await new Promise(setImmediate);
|
||||
assert.deepEqual(events, ['system', ['loading', false]]);
|
||||
});
|
||||
Reference in New Issue
Block a user