kt-template-online-api/test/qqbot/napcat/qqbot-napcat-container-runtime-status.spec.ts

314 lines
8.9 KiB
TypeScript

jest.mock('@/common', () => {
const actualCommon = jest.requireActual('@/common');
return {
...actualCommon,
throwVbenError: (message: string) => {
throw new Error(message);
},
};
});
import { ToolsService } from '@/common';
import { QqbotNapcatContainerService } from '@/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service';
describe('QqbotNapcatContainerService runtime status', () => {
const createService = (repository: { update: jest.Mock }) =>
new QqbotNapcatContainerService(
{ get: jest.fn() } as any,
repository as any,
{} as any,
new ToolsService(),
);
it('keeps WebUI configuration errors out of the QQ login message', async () => {
const repository = {
update: jest.fn(),
};
const service = createService(repository);
const snapshot = await service.inspectRuntimeStatus({
id: 'container-1',
lastError: null,
status: 'running',
} as any);
expect(snapshot).toEqual(
expect.objectContaining({
lastError: 'NapCat WebUI 配置缺失',
qqLoginMessage: null,
qqLoginStatus: 'unknown',
webuiOnline: false,
}),
);
expect(repository.update).toHaveBeenCalledWith(
{ id: 'container-1' },
expect.objectContaining({
lastError: 'NapCat WebUI 配置缺失',
}),
);
});
it('keeps WebUI request errors out of the QQ login message', async () => {
const repository = {
update: jest.fn(),
};
const service = createService(repository);
jest
.spyOn(service as any, 'getNapcatCredential')
.mockRejectedValue(new Error('NapCat WebUI 请求超时'));
const snapshot = await service.inspectRuntimeStatus({
baseUrl: 'http://127.0.0.1:6100/',
id: 'container-1',
lastError: null,
status: 'running',
webuiToken: 'token',
} as any);
expect(snapshot).toEqual(
expect.objectContaining({
lastError: 'NapCat WebUI 请求超时',
qqLoginMessage: null,
qqLoginStatus: 'unknown',
webuiOnline: false,
}),
);
});
it('reuses WebUI credential when checking the same running container repeatedly', async () => {
const repository = {
update: jest.fn(),
};
const service = createService(repository);
const requestNapcat = jest
.spyOn(service as any, 'requestNapcat')
.mockImplementation(async (_runtime, path: string) => {
if (path === '/api/auth/login') {
return { Credential: 'credential-1' };
}
if (path === '/api/QQLogin/CheckLoginStatus') {
return {
isLogin: true,
isOffline: false,
loginError: '',
online: true,
qrcodeurl: '',
};
}
throw new Error(`unexpected path ${path}`);
});
const container = {
baseUrl: 'http://127.0.0.1:6100/',
id: 'container-1',
lastError: null,
name: 'napcat-1',
status: 'running',
webuiPort: 6100,
webuiToken: 'token',
} as any;
await service.inspectRuntimeStatus(container);
await service.inspectRuntimeStatus(container);
expect(
requestNapcat.mock.calls.filter(
([, path]: unknown[]) => path === '/api/auth/login',
),
).toHaveLength(1);
expect(
requestNapcat.mock.calls.filter(
([, path]: unknown[]) => path === '/api/QQLogin/CheckLoginStatus',
),
).toHaveLength(2);
});
it('deduplicates concurrent WebUI credential requests for the same container', async () => {
const repository = {
update: jest.fn(),
};
const service = createService(repository);
const requestNapcat = jest
.spyOn(service as any, 'requestNapcat')
.mockImplementation(async (_runtime, path: string) => {
if (path === '/api/auth/login') {
await new Promise((resolve) => setTimeout(resolve, 1));
return { Credential: 'credential-1' };
}
if (path === '/api/QQLogin/CheckLoginStatus') {
return {
isLogin: true,
isOffline: false,
loginError: '',
online: true,
qrcodeurl: '',
};
}
throw new Error(`unexpected path ${path}`);
});
const container = {
baseUrl: 'http://127.0.0.1:6100/',
id: 'container-1',
lastError: null,
name: 'napcat-1',
status: 'running',
webuiPort: 6100,
webuiToken: 'token',
} as any;
await Promise.all([
service.inspectRuntimeStatus(container),
service.inspectRuntimeStatus(container),
]);
expect(
requestNapcat.mock.calls.filter(
([, path]: unknown[]) => path === '/api/auth/login',
),
).toHaveLength(1);
expect(
requestNapcat.mock.calls.filter(
([, path]: unknown[]) => path === '/api/QQLogin/CheckLoginStatus',
),
).toHaveLength(2);
});
it('refreshes cached WebUI credential once when NapCat rejects status auth', async () => {
const repository = {
update: jest.fn(),
};
const service = createService(repository);
let credentialIndex = 0;
let statusChecks = 0;
const requestNapcat = jest
.spyOn(service as any, 'requestNapcat')
.mockImplementation(
async (_runtime, path: string, _body, credential?: string) => {
if (path === '/api/auth/login') {
credentialIndex += 1;
return { Credential: `credential-${credentialIndex}` };
}
if (path === '/api/QQLogin/CheckLoginStatus') {
statusChecks += 1;
if (statusChecks > 1 && credential === 'credential-1') {
throw new Error('Unauthorized');
}
return {
isLogin: true,
isOffline: false,
loginError: '',
online: true,
qrcodeurl: '',
};
}
throw new Error(`unexpected path ${path}`);
},
);
const container = {
baseUrl: 'http://127.0.0.1:6100/',
id: 'container-1',
lastError: null,
name: 'napcat-1',
status: 'running',
webuiPort: 6100,
webuiToken: 'token',
} as any;
await service.inspectRuntimeStatus(container);
const snapshot = await service.inspectRuntimeStatus(container);
expect(snapshot).toEqual(
expect.objectContaining({
qqLoginStatus: 'online',
webuiOnline: true,
}),
);
expect(
requestNapcat.mock.calls.filter(
([, path]: unknown[]) => path === '/api/auth/login',
),
).toHaveLength(2);
expect(
requestNapcat.mock.calls.filter(
([, path]: unknown[]) => path === '/api/QQLogin/CheckLoginStatus',
),
).toHaveLength(3);
});
it('keeps a refreshed credential when a later stale status request is rejected', async () => {
const repository = {
update: jest.fn(),
};
const service = createService(repository);
let credentialIndex = 0;
let staleMode = false;
let staleStatusChecks = 0;
let releaseSecondStaleReject: (() => void) | undefined;
const secondStaleReject = new Promise<void>((resolve) => {
releaseSecondStaleReject = resolve;
});
const requestNapcat = jest
.spyOn(service as any, 'requestNapcat')
.mockImplementation(
async (_runtime, path: string, _body, credential?: string) => {
if (path === '/api/auth/login') {
credentialIndex += 1;
return {
Credential:
credentialIndex === 1
? 'credential-old'
: 'credential-new',
};
}
if (path === '/api/QQLogin/CheckLoginStatus') {
if (staleMode && credential === 'credential-old') {
staleStatusChecks += 1;
if (staleStatusChecks === 1) {
throw new Error('Unauthorized');
}
await secondStaleReject;
throw new Error('Unauthorized');
}
if (staleMode && credential === 'credential-new') {
releaseSecondStaleReject?.();
}
return {
isLogin: true,
isOffline: false,
loginError: '',
online: true,
qrcodeurl: '',
};
}
throw new Error(`unexpected path ${path}`);
},
);
const container = {
baseUrl: 'http://127.0.0.1:6100/',
id: 'container-1',
lastError: null,
name: 'napcat-1',
status: 'running',
webuiPort: 6100,
webuiToken: 'token',
} as any;
await service.inspectRuntimeStatus(container);
staleMode = true;
await Promise.all([
service.inspectRuntimeStatus(container),
service.inspectRuntimeStatus(container),
]);
expect(
requestNapcat.mock.calls.filter(
([, path]: unknown[]) => path === '/api/auth/login',
),
).toHaveLength(2);
});
});