mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-22 22:05:02 +03:00
a8d0700bf2
* feat(auth): add node-admin API token scope Keep ordinary node-sync credentials unable to self-update a remote panel while allowing explicitly elevated node administrators to inherit synchronization access and invoke updatePanel. * style(docs): format API scope table * fix(auth): preserve mTLS node-sync semantics Allow only an explicit node-admin bearer to elevate a verified mTLS request. Existing admin, monitor, and node-sync bearer values continue to use the historical node-sync identity on mTLS connections. * chore(ci): rerun flaky checks --------- Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com>
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { AllSetting } from '@/models/setting';
|
|
import SecurityTab from '@/pages/settings/SecurityTab';
|
|
import { HttpUtil } from '@/utils';
|
|
|
|
describe('API token creation date', () => {
|
|
it('renders both API seconds and legacy millisecond timestamps', async () => {
|
|
vi.spyOn(HttpUtil, 'get').mockResolvedValueOnce({
|
|
success: true,
|
|
msg: '',
|
|
obj: [
|
|
{
|
|
id: 2,
|
|
name: 'seconds-token',
|
|
enabled: true,
|
|
createdAt: 1782485394,
|
|
scope: 'node-sync',
|
|
expiresAt: 0,
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'legacy-milliseconds-token',
|
|
enabled: true,
|
|
createdAt: 1782485394270,
|
|
scope: 'node-admin',
|
|
expiresAt: 0,
|
|
},
|
|
],
|
|
});
|
|
|
|
render(
|
|
<SecurityTab allSetting={{} as AllSetting} updateSetting={vi.fn()} saveSetting={vi.fn()} />,
|
|
);
|
|
fireEvent.click(screen.getByRole('tab', { name: /API Token/ }));
|
|
|
|
expect(await screen.findByText('seconds-token')).toBeTruthy();
|
|
expect(screen.getByText('legacy-milliseconds-token')).toBeTruthy();
|
|
expect(screen.getAllByText(/2026/)).toHaveLength(2);
|
|
expect(screen.getByText('Node synchronization')).toBeTruthy();
|
|
expect(screen.getByText('Node administrator')).toBeTruthy();
|
|
});
|
|
|
|
it('creates a token with the selected scope', async () => {
|
|
vi.spyOn(HttpUtil, 'get').mockResolvedValue({ success: true, msg: '', obj: [] });
|
|
const post = vi.spyOn(HttpUtil, 'post').mockResolvedValue({
|
|
success: true,
|
|
msg: '',
|
|
obj: { token: 'created-token' },
|
|
});
|
|
|
|
render(
|
|
<SecurityTab allSetting={{} as AllSetting} updateSetting={vi.fn()} saveSetting={vi.fn()} />,
|
|
);
|
|
fireEvent.click(screen.getByRole('tab', { name: /API Token/ }));
|
|
fireEvent.click(await screen.findByRole('button', { name: /New token/ }));
|
|
fireEvent.change(screen.getByPlaceholderText('e.g. central-panel-a'), {
|
|
target: { value: 'central-node' },
|
|
});
|
|
|
|
const scope = screen.getByRole('combobox', { name: 'Scope' });
|
|
fireEvent.mouseDown(scope.closest('.ant-select') ?? scope);
|
|
const option = Array.from(document.querySelectorAll('.ant-select-item-option')).find(
|
|
(item) => item.textContent === 'Node administrator',
|
|
);
|
|
if (!option) throw new Error('Node administrator option not found');
|
|
fireEvent.click(option);
|
|
expect(
|
|
screen.getByText(
|
|
'Includes node synchronization permissions and can also start panel software updates.',
|
|
),
|
|
).toBeTruthy();
|
|
|
|
fireEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Confirm' }));
|
|
await waitFor(() =>
|
|
expect(post).toHaveBeenCalledWith('/panel/api/setting/apiTokens/create', {
|
|
name: 'central-node',
|
|
scope: 'node-admin',
|
|
}),
|
|
);
|
|
});
|
|
});
|