286 lines
8.7 KiB
TypeScript
286 lines
8.7 KiB
TypeScript
import {
|
|
cancelQqbotAccountScan,
|
|
createQqbotNapcatWebuiSession,
|
|
getNapcatLoginProgressLabel,
|
|
getNapcatNewDeviceStatusMessage,
|
|
getQqbotAccountScanEventsUrl,
|
|
getQqbotAccountScanStatus,
|
|
getQqbotNapcatRuntimeDetail,
|
|
heartbeatQqbotNapcatWebuiSession,
|
|
mergeNapcatAccountScanResult,
|
|
NAPCAT_LOGIN_PROGRESS_LABELS,
|
|
refreshQqbotAccountScanQrcode,
|
|
resolveNapcatLoginDisplayQrcode,
|
|
revokeQqbotNapcatWebuiSession,
|
|
startQqbotAccountScanCreate,
|
|
startQqbotAccountScanRefresh,
|
|
submitQqbotAccountScanCaptcha,
|
|
} from '@test-source/apps/web-antdv-next/src/api/qqbot/napcat';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { requestClient } from '#/api/request';
|
|
|
|
vi.mock('#/api/request', () => ({
|
|
requestClient: {
|
|
get: vi.fn(),
|
|
getBaseUrl: vi.fn(() => '/api'),
|
|
post: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('napcat login display helpers', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('keeps all Chinese progress labels required by the login flow', () => {
|
|
expect(NAPCAT_LOGIN_PROGRESS_LABELS).toMatchObject({
|
|
'captcha-submit': '验证码已提交,等待确认',
|
|
'login-failed': '登录失败',
|
|
'login-success': '登录成功',
|
|
'manual-qr-required': '正在生成手动二维码',
|
|
'new-device-confirming': '新设备确认中',
|
|
'new-device-qrcode-ready': '新设备二维码待扫码',
|
|
'new-device-required': '需要新设备验证二维码',
|
|
'new-device-scanned': '新设备二维码已扫码',
|
|
'new-device-verified': '新设备验证成功,继续登录',
|
|
'password-login': '正在密码登录',
|
|
'password-login-captcha': '需要验证码',
|
|
'password-login-captcha-submit': '验证码已提交,等待确认',
|
|
'password-login-failed': '登录失败',
|
|
'password-login-start': '正在密码登录',
|
|
'quick-login-fallback': '快速登录失败,进入密码登录',
|
|
'quick-login-start': '正在快速登录',
|
|
'runtime-cleanup-failed': '运行态清理失败',
|
|
});
|
|
});
|
|
|
|
it('shows new-device QR separately before normal login QR or captcha', () => {
|
|
expect(
|
|
resolveNapcatLoginDisplayQrcode({
|
|
captchaUrl: 'https://captcha.qq.com/',
|
|
newDeviceQrcode: 'data:image/png;base64,new-device',
|
|
qrcode: 'normal-login-qrcode',
|
|
}),
|
|
).toBe('data:image/png;base64,new-device');
|
|
expect(getNapcatNewDeviceStatusMessage('scanned')).toBe(
|
|
'新设备二维码已扫码',
|
|
);
|
|
});
|
|
|
|
it('preserves pending captcha and new-device evidence when later polls omit URLs', () => {
|
|
expect(
|
|
mergeNapcatAccountScanResult(
|
|
{
|
|
captchaUrl: 'https://captcha.qq.com/proof',
|
|
mode: 'refresh',
|
|
sessionId: 'scan-1',
|
|
status: 'pending',
|
|
},
|
|
{
|
|
mode: 'refresh',
|
|
sessionId: 'scan-1',
|
|
status: 'pending',
|
|
},
|
|
),
|
|
).toMatchObject({
|
|
captchaUrl: 'https://captcha.qq.com/proof',
|
|
status: 'pending',
|
|
});
|
|
|
|
expect(
|
|
mergeNapcatAccountScanResult(
|
|
{
|
|
deviceVerifyUrl: 'https://ti.qq.com/device',
|
|
mode: 'refresh',
|
|
newDeviceQrcode: 'data:image/png;base64,new-device',
|
|
newDeviceStatus: 'qr-pending',
|
|
sessionId: 'scan-1',
|
|
status: 'pending',
|
|
},
|
|
{
|
|
mode: 'refresh',
|
|
newDeviceStatus: 'scanned',
|
|
sessionId: 'scan-1',
|
|
status: 'pending',
|
|
},
|
|
),
|
|
).toMatchObject({
|
|
deviceVerifyUrl: 'https://ti.qq.com/device',
|
|
newDeviceQrcode: 'data:image/png;base64,new-device',
|
|
newDeviceStatus: 'scanned',
|
|
status: 'pending',
|
|
});
|
|
});
|
|
|
|
it('clears stale verification evidence when scan reaches a terminal state', () => {
|
|
expect(
|
|
mergeNapcatAccountScanResult(
|
|
{
|
|
captchaUrl: 'https://captcha.qq.com/proof',
|
|
deviceVerifyUrl: 'https://ti.qq.com/device',
|
|
mode: 'refresh',
|
|
newDeviceQrcode: 'data:image/png;base64,new-device',
|
|
newDeviceStatus: 'confirming',
|
|
sessionId: 'scan-1',
|
|
status: 'pending',
|
|
},
|
|
{
|
|
mode: 'refresh',
|
|
sessionId: 'scan-1',
|
|
status: 'success',
|
|
},
|
|
),
|
|
).toMatchObject({
|
|
captchaUrl: undefined,
|
|
deviceVerifyUrl: undefined,
|
|
newDeviceQrcode: undefined,
|
|
newDeviceStatus: undefined,
|
|
status: 'success',
|
|
});
|
|
});
|
|
|
|
it('keeps the complete new-device progress key set in caller helpers', () => {
|
|
expect(
|
|
Object.keys(NAPCAT_LOGIN_PROGRESS_LABELS)
|
|
.filter((key) => key.startsWith('new-device-'))
|
|
.toSorted(),
|
|
).toEqual([
|
|
'new-device-confirming',
|
|
'new-device-qrcode-ready',
|
|
'new-device-required',
|
|
'new-device-scanned',
|
|
'new-device-verified',
|
|
]);
|
|
});
|
|
|
|
it('falls back to Chinese progress labels by SSE step key', () => {
|
|
expect(
|
|
getNapcatLoginProgressLabel({
|
|
message: 'Need new device',
|
|
step: 'new-device-required',
|
|
}),
|
|
).toBe('需要新设备验证二维码');
|
|
expect(
|
|
getNapcatLoginProgressLabel({
|
|
message: '',
|
|
step: 'new-device-confirming',
|
|
}),
|
|
).toBe('新设备确认中');
|
|
});
|
|
|
|
it('owns create, refresh, status, QR refresh, captcha, cancel, and SSE caller routes', async () => {
|
|
const scanResult = {
|
|
mode: 'refresh' as const,
|
|
sessionId: 'scan-1',
|
|
status: 'pending' as const,
|
|
};
|
|
vi.mocked(requestClient.post).mockResolvedValue(scanResult);
|
|
vi.mocked(requestClient.get).mockResolvedValue(scanResult);
|
|
|
|
await expect(startQqbotAccountScanCreate()).resolves.toBe(scanResult);
|
|
await expect(startQqbotAccountScanRefresh('account-1')).resolves.toBe(
|
|
scanResult,
|
|
);
|
|
await expect(getQqbotAccountScanStatus('scan-1')).resolves.toBe(scanResult);
|
|
await expect(refreshQqbotAccountScanQrcode('scan-1')).resolves.toBe(
|
|
scanResult,
|
|
);
|
|
await expect(
|
|
submitQqbotAccountScanCaptcha({
|
|
randstr: '@rand',
|
|
sessionId: 'scan-1',
|
|
ticket: 'ticket',
|
|
}),
|
|
).resolves.toBe(scanResult);
|
|
await expect(cancelQqbotAccountScan('scan-1')).resolves.toBe(scanResult);
|
|
|
|
expect(requestClient.post).toHaveBeenCalledWith(
|
|
'/qqbot/account/scan/create',
|
|
);
|
|
expect(requestClient.post).toHaveBeenCalledWith(
|
|
'/qqbot/account/scan/refresh?id=account-1',
|
|
);
|
|
expect(requestClient.get).toHaveBeenCalledWith(
|
|
'/qqbot/account/scan/status',
|
|
{ params: { sessionId: 'scan-1' } },
|
|
);
|
|
expect(requestClient.post).toHaveBeenCalledWith(
|
|
'/qqbot/account/scan/qrcode/refresh?sessionId=scan-1',
|
|
);
|
|
expect(requestClient.post).toHaveBeenCalledWith(
|
|
'/qqbot/account/scan/captcha/submit',
|
|
{
|
|
randstr: '@rand',
|
|
sessionId: 'scan-1',
|
|
ticket: 'ticket',
|
|
},
|
|
);
|
|
expect(requestClient.post).toHaveBeenCalledWith(
|
|
'/qqbot/account/scan/cancel?sessionId=scan-1',
|
|
);
|
|
expect(getQqbotAccountScanEventsUrl('scan 1')).toBe(
|
|
'/api/qqbot/account/scan/events?sessionId=scan%201',
|
|
);
|
|
});
|
|
|
|
it('builds the read-only NapCat runtime detail request', async () => {
|
|
vi.mocked(requestClient.get).mockResolvedValue({});
|
|
|
|
await getQqbotNapcatRuntimeDetail('account-1');
|
|
|
|
expect(requestClient.get).toHaveBeenCalledWith(
|
|
'/qqbot/napcat/runtime/detail',
|
|
{
|
|
params: { accountId: 'account-1' },
|
|
},
|
|
);
|
|
});
|
|
|
|
it('owns NapCat WebUI gateway session caller routes', async () => {
|
|
const sessionResult = {
|
|
account: {
|
|
id: 'account-1',
|
|
name: 'preview',
|
|
selfId: '10001',
|
|
},
|
|
container: {
|
|
webuiStatus: 'online' as const,
|
|
},
|
|
expiresAt: 1_782_000_000,
|
|
iframeUrl: '/qqbot/napcat/webui/session/session-1/',
|
|
sessionId: 'session-1',
|
|
};
|
|
const lifecycleResult = {
|
|
sessionId: 'session-1',
|
|
status: 'active' as const,
|
|
};
|
|
vi.mocked(requestClient.post)
|
|
.mockResolvedValueOnce(sessionResult)
|
|
.mockResolvedValueOnce(lifecycleResult)
|
|
.mockResolvedValueOnce({ ...lifecycleResult, status: 'revoked' });
|
|
|
|
await expect(
|
|
createQqbotNapcatWebuiSession({ accountId: 'account-1' }),
|
|
).resolves.toBe(sessionResult);
|
|
await expect(heartbeatQqbotNapcatWebuiSession('session-1')).resolves.toBe(
|
|
lifecycleResult,
|
|
);
|
|
await expect(revokeQqbotNapcatWebuiSession('session-1')).resolves.toEqual({
|
|
sessionId: 'session-1',
|
|
status: 'revoked',
|
|
});
|
|
|
|
expect(requestClient.post).toHaveBeenCalledWith(
|
|
'/qqbot/napcat/webui/session',
|
|
{ accountId: 'account-1' },
|
|
);
|
|
expect(requestClient.post).toHaveBeenCalledWith(
|
|
'/qqbot/napcat/webui/session/session-1/heartbeat',
|
|
);
|
|
expect(requestClient.post).toHaveBeenCalledWith(
|
|
'/qqbot/napcat/webui/session/session-1/revoke',
|
|
);
|
|
});
|
|
});
|