Files
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

179 lines
7.0 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
GROUP_NOTICE_TYPE_NEW_MEMBERS,
calculateBkn,
getGroupNoticeWebAPI,
groupNoticeImageUrl,
parseGroupNoticeImageUploadResponse,
resolveGroupNoticeOptions,
setGroupNoticeWebAPI,
type WebApiGroupNoticeRet,
} from '@snowluma/protocol/web/group-notice';
import { RequestUtil } from '@snowluma/protocol/web/request-util';
describe('group-notice / publish HTTP layer', () => {
afterEach(() => vi.restoreAllMocks());
const cookies = { skey: 'BODY_KEY', p_skey: 'URL_KEY', uin: 'o10000' };
it('publishes a regular announcement with the exact options and image metadata', async () => {
const request = vi.spyOn(RequestUtil, 'HttpGetJson').mockResolvedValue({ ec: 0 } as never);
await setGroupNoticeWebAPI(cookies, '12345', 'regular notice', {
pinned: 1,
isShowEditCard: 0,
tipWindowType: 0,
confirmRequired: 0,
picId: 'pic-id',
imgWidth: 640,
imgHeight: 360,
});
const [url, method, rawBody, headers, isJsonRet, isArgJson] = request.mock.calls[0]!;
expect(url).toBe(
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${calculateBkn('URL_KEY')}`,
);
expect(method).toBe('POST');
const body = new URLSearchParams(rawBody as string);
expect(Object.fromEntries(body)).toMatchObject({
qid: '12345',
bkn: calculateBkn('BODY_KEY'),
text: 'regular notice',
pinned: '1',
type: '1',
pic: 'pic-id',
imgWidth: '640',
imgHeight: '360',
});
expect(JSON.parse(body.get('settings')!)).toEqual({
is_show_edit_card: 0,
tip_window_type: 0,
confirm_required: 0,
});
expect(headers).toMatchObject({ 'Content-Type': 'application/x-www-form-urlencoded' });
expect(isJsonRet).toBe(true);
expect(isArgJson).toBe(false);
});
it('routes semantic new-member announcements to the instruction endpoint with type 20', async () => {
const request = vi.spyOn(RequestUtil, 'HttpGetJson').mockResolvedValue({ ec: 0 } as never);
await setGroupNoticeWebAPI(cookies, '12345', 'welcome', { sendToNewMembers: true });
const [url, , rawBody] = request.mock.calls[0]!;
expect(url).toContain('/cgi-bin/announce/add_qun_instruction?');
expect(new URLSearchParams(rawBody as string).get('type')).toBe(
String(GROUP_NOTICE_TYPE_NEW_MEMBERS),
);
});
it('keeps raw type=20 compatibility while selecting the correct endpoint', async () => {
const request = vi.spyOn(RequestUtil, 'HttpGetJson').mockResolvedValue({ ec: 0 } as never);
await setGroupNoticeWebAPI(cookies, '12345', 'welcome', { type: 20 });
expect(request.mock.calls[0]![0]).toContain('/cgi-bin/announce/add_qun_instruction?');
});
it('fails before I/O when raw type and semantic target conflict', async () => {
const request = vi.spyOn(RequestUtil, 'HttpGetJson');
await expect(
setGroupNoticeWebAPI(cookies, '12345', 'bad', {
type: 1,
sendToNewMembers: true,
}),
).rejects.toThrow('conflicts');
expect(request).not.toHaveBeenCalled();
});
it('rejects unsupported raw types and non-binary options', () => {
expect(() => resolveGroupNoticeOptions({ type: 6 })).toThrow('type must be 1');
expect(() => resolveGroupNoticeOptions({ pinned: 2 })).toThrow('pinned must be 0 or 1');
expect(() => resolveGroupNoticeOptions({ tipWindowType: -1 })).toThrow(
'tipWindowType must be 0 or 1',
);
});
it('preserves the established regular-announcement defaults', () => {
expect(resolveGroupNoticeOptions()).toMatchObject({
pinned: 0,
type: 1,
sendToNewMembers: false,
isShowEditCard: 1,
tipWindowType: 1,
confirmRequired: 1,
});
});
it('propagates transport failures instead of returning undefined', async () => {
vi.spyOn(RequestUtil, 'HttpGetJson').mockRejectedValue(
new Error('Unexpected status code: 403'),
);
await expect(setGroupNoticeWebAPI(cookies, '12345', 'notice')).rejects.toThrow('403');
});
});
describe('group-notice / image download url', () => {
it('builds the public CDN url from a picture id', () => {
expect(groupNoticeImageUrl('pic-1')).toBe('https://gdynamic.qpic.cn/gdynamic/pic-1/0');
});
it('rejects empty or path-like ids', () => {
expect(() => groupNoticeImageUrl('')).toThrow('empty');
expect(() => groupNoticeImageUrl(' ')).toThrow('empty');
expect(() => groupNoticeImageUrl('a/b')).toThrow('malformed');
expect(() => groupNoticeImageUrl('a?x=1')).toThrow('malformed');
});
});
describe('group-notice / image upload response', () => {
it('parses HTML-escaped image metadata', () => {
expect(
parseGroupNoticeImageUploadResponse(
JSON.stringify({
ec: 0,
id: '{"id":"pic-1","w":"640","h":"360"}',
}),
),
).toEqual({ id: 'pic-1', width: 640, height: 360 });
});
it('surfaces server and malformed-response failures', () => {
expect(() =>
parseGroupNoticeImageUploadResponse(JSON.stringify({ ec: 14, em: 'denied' })),
).toThrow('ec=14 em=denied');
expect(() => parseGroupNoticeImageUploadResponse('not json')).toThrow('invalid JSON');
expect(() =>
parseGroupNoticeImageUploadResponse(
JSON.stringify({ ec: 0, id: '{"id":"pic","w":0,"h":10}' }),
),
).toThrow('malformed image metadata');
});
});
describe('group-notice / list HTTP layer', () => {
afterEach(() => vi.restoreAllMocks());
it('requests both regular and new-member collections and preserves server errors', async () => {
const response: WebApiGroupNoticeRet = { ec: 14, em: 'permission denied' };
const request = vi.spyOn(RequestUtil, 'HttpGetJson').mockResolvedValue(response as never);
await expect(getGroupNoticeWebAPI({ skey: 'SK', p_skey: 'PSK' }, '9876')).resolves.toBe(
response,
);
const [url, method, rawBody] = request.mock.calls[0]!;
expect(url).toContain('/cgi-bin/announce/list_announce?');
expect(method).toBe('POST');
const body = new URLSearchParams(rawBody as string);
expect(body.get('i')).toBe('1');
expect(body.get('ni')).toBe('1');
});
it('propagates transport failures instead of hiding them as an empty list', async () => {
vi.spyOn(RequestUtil, 'HttpGetJson').mockRejectedValue(new Error('socket closed'));
await expect(getGroupNoticeWebAPI({ skey: 'SK' }, '9876')).rejects.toThrow('socket closed');
});
});