Files
SnowLuma/packages/sdk/tests/http-client.test.ts
T
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

309 lines
11 KiB
TypeScript

import { describe, expect, expectTypeOf, it, vi } from 'vitest';
import type { GroupSystemMessage, QzoneMsgDetailResult, QzoneProfileResult } from '../src';
import {
SnowLumaAbortError,
SnowLumaApiError,
SnowLumaAuthError,
SnowLumaHttpClient,
SnowLumaParseError,
} from '../src';
describe('SnowLumaHttpClient', () => {
it('retains independent group request actors and supports old servers', async () => {
const legacy: GroupSystemMessage = {
group_id: 90001,
group_name: 'group',
request_id: 1,
requester_uin: 10002,
requester_nick: 'member',
invitor_uin: 10004,
invitor_nick: 'inviter',
message: '',
checked: true,
flag: 'request-1',
};
const current: GroupSystemMessage = {
...legacy,
target_uin: 10002,
target_status: 'known',
operator_uin: 10003,
operator_status: 'known',
invitor_status: 'known',
};
const fetchImpl = vi.fn(
async () =>
new Response(JSON.stringify({ status: 'ok', retcode: 0, data: [current, legacy] })),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
const result = await client.getGroupSystemMessages({ groupId: 90001 });
expectTypeOf(result).toEqualTypeOf<GroupSystemMessage[]>();
expect(result).toEqual([current, legacy]);
expect(result[1]).not.toHaveProperty('invitor_status');
});
it('reads typed space profiles for the current and an explicit account', async () => {
const data: QzoneProfileResult = { name: 'Space', description: 'Description' };
const fetchImpl = vi
.fn()
.mockImplementation(
async () => new Response(JSON.stringify({ status: 'ok', retcode: 0, data })),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
const own = await client.raw('get_qzone_profile', {});
const other = await client.raw('get_qzone_profile', { target_uin: '10002' });
expectTypeOf(own).toEqualTypeOf<QzoneProfileResult>();
expect(other).toEqual(data);
expect(
fetchImpl.mock.calls.map((call) => JSON.parse((call[1] as RequestInit).body as string)),
).toEqual([
{ action: 'get_qzone_profile', params: {} },
{ action: 'get_qzone_profile', params: { target_uin: '10002' } },
]);
});
it('preserves omitted and empty space profile fields and returns null', async () => {
const fetchImpl = vi
.fn()
.mockImplementation(
async () => new Response(JSON.stringify({ status: 'ok', retcode: 0, data: null })),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
const result = await client.raw('set_qzone_profile', { name: 'Space' });
expectTypeOf(result).toEqualTypeOf<null>();
expect(result).toBeNull();
await expect(client.raw('set_qzone_profile', { description: '' })).resolves.toBeNull();
expect(
fetchImpl.mock.calls.map((call) => JSON.parse((call[1] as RequestInit).body as string)),
).toEqual([
{ action: 'set_qzone_profile', params: { name: 'Space' } },
{ action: 'set_qzone_profile', params: { description: '' } },
]);
});
it('propagates space profile update failures', async () => {
const fetchImpl = vi.fn(
async () =>
new Response(
JSON.stringify({
status: 'failed',
retcode: 1200,
data: null,
wording: 'Qzone requires verification',
}),
),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
const request = client.raw('set_qzone_profile', { name: 'Space' });
await expect(request).rejects.toMatchObject({
name: 'SnowLumaApiError',
retcode: 1200,
wording: 'Qzone requires verification',
});
await expect(request).rejects.toBeInstanceOf(SnowLumaApiError);
});
it('reads a typed Qzone comment page through raw', async () => {
const data: QzoneMsgDetailResult = {
tid: 'T1',
content: 'Post',
comment_num: 1,
comments: [
{
uin: 20002,
name: 'Reader',
time: 1700000000,
content: 'Hi',
comment_id: '0',
replies: [],
},
],
};
const fetchImpl = vi.fn(
async () => new Response(JSON.stringify({ status: 'ok', retcode: 0, data })),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
const params = { tid: 'T1', target_uin: 10001, pos: 0, num: 20 };
const result = await client.raw('get_qzone_msg_detail', params);
expectTypeOf(result).toEqualTypeOf<QzoneMsgDetailResult>();
expect(result).toEqual(data);
expect(fetchImpl).toHaveBeenCalledWith(
'http://127.0.0.1:3000/',
expect.objectContaining({
body: JSON.stringify({ action: 'get_qzone_msg_detail', params }),
}),
);
});
it('requests group share links with default and explicit options', async () => {
const url = 'https://qm.qq.com/q/example';
const fetchImpl = vi
.fn()
.mockImplementation(
async () =>
new Response(JSON.stringify({ status: 'ok', retcode: 0, data: { url } })),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
await expect(client.getGroupShareLink(12345)).resolves.toEqual({ url });
await expect(
client.getGroupShareLink(12345, {
needShortUrl: false,
srcId: 0,
additionalParam: 'source=二维码',
}),
).resolves.toEqual({ url });
expect(
fetchImpl.mock.calls.map((call) => JSON.parse((call[1] as RequestInit).body as string)),
).toEqual([
{ action: 'get_group_share_link', params: { group_id: 12345 } },
{
action: 'get_group_share_link',
params: {
group_id: 12345,
need_short_url: false,
src_id: 0,
additional_param: 'source=二维码',
},
},
]);
});
it('maps extended group-info refresh options to OneBot field names', async () => {
const fetchImpl = vi.fn().mockImplementation(
async () =>
new Response(
JSON.stringify({
status: 'ok',
retcode: 0,
data: null,
}),
),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
await client.getGroupInfoEx(123456, { noCache: true });
await client.getGroupDetailInfo(123456, { noCache: true });
expect(
fetchImpl.mock.calls.map((call) => JSON.parse((call[1] as RequestInit).body as string)),
).toEqual([
{ action: 'get_group_info_ex', params: { group_id: 123456, no_cache: true } },
{ action: 'get_group_detail_info', params: { group_id: 123456, no_cache: true } },
]);
});
it('maps system-face catalog helpers to their OneBot actions', async () => {
const fetchImpl = vi.fn().mockImplementation(
async () =>
new Response(
JSON.stringify({
status: 'ok',
retcode: 0,
data: null,
}),
),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
await client.fetchSysFaces(true);
await client.fetchFaceEntity(392, { refresh: true });
await client.searchSysFaces('dragon');
await client.fetchSuperFaceId(392);
expect(
fetchImpl.mock.calls.map((call) => JSON.parse((call[1] as RequestInit).body as string)),
).toEqual([
{ action: 'fetch_sys_faces', params: { refresh: true } },
{ action: 'fetch_face_entity', params: { face_id: 392, refresh: true } },
{ action: 'search_sys_faces', params: { query: 'dragon' } },
{ action: 'fetch_super_face_id', params: { face_id: 392, refresh: false } },
]);
});
it('maps group-announcement options to their OneBot field names', async () => {
const fetchImpl = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
status: 'ok',
retcode: 0,
data: null,
}),
),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
await client.sendGroupNotice(941657197, 'welcome', {
pinned: 1,
type: 20,
sendToNewMembers: true,
isShowEditCard: 0,
tipWindowType: 0,
confirmRequired: 0,
});
const init = fetchImpl.mock.calls[0]![1] as RequestInit;
expect(JSON.parse(init.body as string)).toEqual({
action: '_send_group_notice',
params: {
group_id: 941657197,
content: 'welcome',
pinned: 1,
type: 20,
send_to_new_members: true,
is_show_edit_card: 0,
tip_window_type: 0,
confirm_required: 0,
},
});
});
it('throws SnowLumaAuthError for auth retcodes', async () => {
const fetchImpl = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
status: 'failed',
retcode: 1401,
data: null,
wording: 'unauthorized',
}),
),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
await expect(client.getStatus()).rejects.toBeInstanceOf(SnowLumaAuthError);
});
it('throws SnowLumaParseError for non-JSON responses', async () => {
const fetchImpl = vi.fn().mockResolvedValue(new Response('not json', { status: 200 }));
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
await expect(client.getStatus()).rejects.toBeInstanceOf(SnowLumaParseError);
});
it('supports caller AbortSignal cancellation', async () => {
const fetchImpl = vi.fn(
(_url: string | URL | Request, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
const signal = init?.signal;
const abort = () => reject(new DOMException('aborted', 'AbortError'));
if (signal?.aborted) {
abort();
}
signal?.addEventListener('abort', abort, { once: true });
}),
);
const client = new SnowLumaHttpClient({ fetch: fetchImpl });
const controller = new AbortController();
const request = client.getStatus({ signal: controller.signal });
controller.abort();
await expect(request).rejects.toBeInstanceOf(SnowLumaAbortError);
});
});