kt-template-admin/test/api/qqbot/napcat.spec.ts

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',
);
});
});