Files
LinkPort/web/tests/assets-backup.spec.ts
曾志威 9da95beb42
continuous-integration/drone Build is failing
feat: refine management UI, connection diagnostics and local startup
Migrate the workspace to React and Ant Design, improve account forms and responsive layouts, support outbound diagnostics and SSH fingerprint confirmation, and persist the management startup port. Preserve database migrations, remove S3 settings storage, and improve gateway audit handling.
2026-10-13 19:43:23 +08:00

186 lines
9.5 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { readFile } from 'node:fs/promises';
test('加密配置导出并恢复资产文件、文件名及代理目标引用', async ({ page }) => {
await page.goto('/');
const session = await (await page.request.get('/api/v1/session')).json();
const headers = { 'X-CSRF-Token': session.csrfToken };
const filename = 'backup-cluster.yaml';
const token = 'asset-full-backup-private-token';
const password = 'asset-full-backup-target-password';
const content = `apiVersion: v1
kind: Config
current-context: backup
clusters:
- name: isolated
cluster:
server: https://127.0.0.1:1
contexts:
- name: backup
context:
cluster: isolated
user: backup
users:
- name: backup
user:
token: ${token}
`;
let assetId = '', outboundId = '', targetId = '';
const removeResources = async () => {
for (const endpoint of [`targets/${targetId}`, `outbounds/${outboundId}`, `assets/${assetId}`]) {
expect((await page.request.delete(`/api/v1/${endpoint}`, { headers })).ok()).toBeTruthy();
}
};
const downloadJSON = async () => {
await page.getByRole('button', { name: '导出配置', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '导出配置', exact: true });
await expect(dialog.getByRole('checkbox', { name: '加密文件', exact: true })).toHaveCount(0);
await dialog.getByPlaceholder('导出口令(至少 12 字节)', { exact: true }).fill('asset-backup-test-passphrase');
const downloading = page.waitForEvent('download');
await dialog.getByRole('button', { name: '确认导出', exact: true }).click();
const downloaded = await downloading;
expect(downloaded.suggestedFilename()).toBe('linkport-backup.json');
await expect(dialog).not.toBeVisible();
const path = (await downloaded.path())!;
return { path, text: await readFile(path, 'utf8') };
};
try {
const asset = await page.request.post('/api/v1/assets', { headers, data: {
name: '完整备份文件资产', type: 'kubeconfig', filename, content,
} });
expect(asset.ok()).toBeTruthy();
assetId = (await asset.json()).config.id;
const outbound = await page.request.post('/api/v1/outbounds', { headers, data: {
name: '完整备份资产代理', outbound: {
mode: 'k8s', kubeConfigAssetId: assetId, kubeContext: 'backup', namespace: 'backup', resource: 'statefulset/mongo',
},
} });
expect(outbound.ok()).toBeTruthy();
outboundId = (await outbound.json()).config.id;
const target = await page.request.post('/api/v1/targets', { headers, data: {
name: '完整备份资产目标', type: 'mysql', targetHost: '127.0.0.1', targetPort: 3306, outboundId,
accounts: [{ id: 'backup-account', remark: '备份账号', username: 'backup-user', password }],
} });
expect(target.ok()).toBeTruthy();
targetId = (await target.json()).config.id;
await page.reload();
await page.getByRole('navigation').getByRole('button', { name: '配置管理', exact: true }).click();
await expect(page.getByRole('button', { name: '导出脱敏配置', exact: true })).toHaveCount(0);
const encrypted = await downloadJSON();
expect(JSON.parse(encrypted.text).ciphertext).toBeTruthy();
expect(encrypted.text).not.toContain(token);
expect(encrypted.text).not.toContain(password);
await page.getByPlaceholder('导入口令(加密文件必填)', { exact: true }).fill('asset-backup-test-passphrase');
await removeResources();
await page.getByLabel('导入配置文件', { exact: true }).setInputFiles(encrypted.path);
await page.getByRole('button', { name: '导入配置', exact: true }).click();
const preview = page.getByRole('dialog', { name: '导入配置', exact: true });
for (const id of [assetId, targetId]) {
const row = preview.getByRole('row').filter({ has: page.getByRole('cell', { name: id, exact: true }) });
await expect(row).toContainText('新增');
await row.locator('.el-table__expand-icon').click();
}
await expect(preview).toContainText('敏感内容已变更');
await expect(preview).not.toContainText(token);
await expect(preview).not.toContainText(password);
const importing = page.waitForResponse(response => response.url().endsWith('/configuration/import') && response.request().method() === 'POST');
await preview.getByRole('button', { name: '确认导入', exact: true }).click();
expect((await importing).ok()).toBeTruthy();
await expect(preview).not.toBeVisible();
const restoredAsset = await (await page.request.get(`/api/v1/assets/${assetId}`)).json();
expect(restoredAsset.config).toMatchObject({ id: assetId, name: '完整备份文件资产', type: 'kubeconfig', filename });
expect(restoredAsset.secretStatus.content).toBe(true);
expect(restoredAsset.outboundIds).toContain(outboundId);
expect(restoredAsset.targetIds).toContain(targetId);
expect(JSON.stringify(restoredAsset)).not.toContain(token);
const contexts = await page.request.get(`/api/v1/assets/${assetId}/kubernetes-contexts`);
expect(contexts.ok()).toBeTruthy();
expect(await contexts.json()).toMatchObject({
defaultContext: 'backup', contexts: [{ name: 'backup', cluster: 'isolated', server: 'https://127.0.0.1:1' }],
});
const restoredOutbound = await (await page.request.get(`/api/v1/outbounds/${outboundId}`)).json();
expect(restoredOutbound.config.outbound).toMatchObject({
mode: 'k8s', kubeConfigAssetId: assetId, resource: 'statefulset/mongo', namespace: 'backup',
});
const restoredTarget = await (await page.request.get(`/api/v1/targets/${targetId}`)).json();
expect(restoredTarget.config).toMatchObject({
outboundId, accounts: [{ id: 'backup-account', username: 'backup-user' }],
});
expect(restoredTarget.secretStatus['account:backup-account:password']).toBe(true);
expect(JSON.stringify(restoredTarget)).not.toContain(password);
} finally {
if (targetId) await page.request.delete(`/api/v1/targets/${targetId}`, { headers });
if (outboundId) await page.request.delete(`/api/v1/outbounds/${outboundId}`, { headers });
if (assetId) await page.request.delete(`/api/v1/assets/${assetId}`, { headers });
}
});
test('资产导入预览与配置历史隐藏 Kubeconfig 内容', async ({ page }) => {
await page.goto('/');
const session = await (await page.request.get('/api/v1/session')).json();
const headers = { 'X-CSRF-Token': session.csrfToken };
const name = '资产备份历史验证';
const content = `apiVersion: v1
kind: Config
current-context: test
clusters:
- name: cluster
cluster:
server: https://cluster.invalid
contexts:
- name: test
context:
cluster: cluster
user: user
users:
- name: user
user:
token: backup-original-private-token
`;
let id = '';
try {
const created = await page.request.post('/api/v1/assets', { headers, data: { name, type: 'kubeconfig', content } });
expect(created.ok()).toBeTruthy();
id = (await created.json()).config.id;
const updated = await page.request.put(`/api/v1/assets/${id}`, { headers, data: {
name, type: 'kubeconfig', content: content.replace('backup-original-private-token', 'backup-replacement-private-token'),
} });
expect(updated.ok()).toBeTruthy();
await page.reload();
await page.getByRole('navigation').getByRole('button', { name: '配置管理', exact: true }).click();
await page.locator('.el-select').filter({ has: page.getByRole('combobox', { name: '历史资源类型', exact: true }) }).click();
await page.getByRole('option', { name: '文件资产', exact: true }).click();
await page.locator('.el-select').filter({ has: page.getByRole('combobox', { name: '历史资源', exact: true }) }).click();
await page.getByRole('option', { name, exact: true }).click();
await page.locator('[aria-label="配置历史列表"]').getByRole('button', { name: '查看差异 / 恢复', exact: true }).first().click();
const diff = page.getByRole('dialog', { name: '配置变更对比', exact: true });
await expect(diff).toContainText(name);
await expect(diff).toContainText('/content');
await expect(diff).toContainText('敏感内容已变更');
await expect(diff).not.toContainText('private-token');
await diff.getByRole('button', { name: '恢复此版本', exact: true }).click();
const confirm = page.getByRole('dialog', { name: '恢复版本', exact: true });
await expect(confirm).toContainText(name);
await confirm.getByRole('button', { name: /取消|Cancel/ }).click();
await diff.getByRole('button', { name: '取消', exact: true }).click();
await page.getByLabel('导入配置文件', { exact: true }).setInputFiles({
name: 'asset-import.json', mimeType: 'application/json',
buffer: Buffer.from(JSON.stringify({ version: 2, assets: [{ id, name, type: 'kubeconfig', content }] })),
});
await page.getByRole('button', { name: '导入配置', exact: true }).click();
const preview = page.getByRole('dialog', { name: '导入配置', exact: true });
await expect(preview).toContainText('资产 1 个');
await expect(preview.getByRole('cell', { name: '文件资产', exact: true })).toBeVisible();
await preview.locator('.el-table__expand-icon').click();
await expect(preview).toContainText('/content');
await expect(preview).toContainText('敏感内容已变更');
await expect(preview).not.toContainText('private-token');
await preview.getByRole('button', { name: '取消', exact: true }).click();
} finally {
if (id) await page.request.delete(`/api/v1/assets/${id}`, { headers });
}
});