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
133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import {
|
|
getDaySignedListWebAPI,
|
|
mapDaySignedList,
|
|
type DaySignedInfo,
|
|
type DaySignedListResponse,
|
|
} from '@snowluma/protocol/web/group-signin';
|
|
import { RequestUtil } from '@snowluma/protocol/web/request-util';
|
|
|
|
// The signed-in list comes back from qun.qq.com's GetDaySignedList trpc
|
|
// endpoint nested under response.page[0].infos. We pin the field renames
|
|
// and the rank de-skewing formula NapCat uses ((signInRank - 1) / 2 + 1),
|
|
// because that transform is the only real logic in this otherwise-thin
|
|
// HTTP port and a silent off-by-one would be invisible end-to-end.
|
|
|
|
function resp(infos: DaySignedInfo[]): DaySignedListResponse {
|
|
return {
|
|
retCode: 0,
|
|
response: { page: [{ infos, offset: 0, total: infos.length }] },
|
|
funcCode: 0,
|
|
};
|
|
}
|
|
|
|
describe('group-signin / mapDaySignedList', () => {
|
|
it('renames fields and de-skews signInRank', () => {
|
|
const out = mapDaySignedList(
|
|
resp([
|
|
{
|
|
uid: '10001',
|
|
uidGroupNick: 'Alice',
|
|
signedTimeStamp: '1700000000',
|
|
signInRank: 1,
|
|
},
|
|
{ uid: '10002', uidGroupNick: 'Bob', signedTimeStamp: '1700000050', signInRank: 3 },
|
|
{
|
|
uid: '10003',
|
|
uidGroupNick: 'Carol',
|
|
signedTimeStamp: '1700000100',
|
|
signInRank: 5,
|
|
},
|
|
]),
|
|
);
|
|
expect(out).toEqual([
|
|
{ user_id: 10001, nick: 'Alice', time: 1700000000, rank: 1 },
|
|
{ user_id: 10002, nick: 'Bob', time: 1700000050, rank: 2 },
|
|
{ user_id: 10003, nick: 'Carol', time: 1700000100, rank: 3 },
|
|
]);
|
|
});
|
|
|
|
it('returns [] when infos is empty', () => {
|
|
expect(mapDaySignedList(resp([]))).toEqual([]);
|
|
});
|
|
|
|
it('returns [] when page is missing entirely', () => {
|
|
expect(mapDaySignedList({ retCode: 0, response: {}, funcCode: 0 })).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('group-signin / getDaySignedListWebAPI (HTTP layer)', () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
const cookies = { p_skey: 'PSK', skey: 'SK', uin: 'o10000', p_uin: 'o10000' };
|
|
// g_tk = djb2-31bit over p_skey 'PSK'
|
|
const expectedGtk = (() => {
|
|
let h = 5381;
|
|
|
|
for (const c of 'PSK') {
|
|
h += (h << 5) + c.charCodeAt(0);
|
|
}
|
|
|
|
return (h & 0x7fffffff).toString();
|
|
})();
|
|
|
|
it('POSTs the exact url + json body and maps the result', async () => {
|
|
const spy = vi.spyOn(RequestUtil, 'HttpGetJson').mockResolvedValue({
|
|
retCode: 0,
|
|
funcCode: 0,
|
|
response: {
|
|
page: [
|
|
{
|
|
infos: [
|
|
{
|
|
uid: '777',
|
|
uidGroupNick: 'Z',
|
|
signedTimeStamp: '1700000000',
|
|
signInRank: 3,
|
|
},
|
|
],
|
|
offset: 0,
|
|
total: 1,
|
|
},
|
|
],
|
|
},
|
|
} satisfies DaySignedListResponse as never);
|
|
|
|
const out = await getDaySignedListWebAPI(cookies, '54321', '10000', new Date(2026, 0, 2));
|
|
|
|
expect(out).toEqual([{ user_id: 777, nick: 'Z', time: 1700000000, rank: 2 }]);
|
|
const [url, method, body, headers, isJsonRet, isArgJson] = spy.mock.calls[0]!;
|
|
expect(url).toBe(`https://qun.qq.com/v2/signin/trpc/GetDaySignedList?g_tk=${expectedGtk}`);
|
|
expect(method).toBe('POST');
|
|
expect(body).toEqual({
|
|
dayYmd: '20260102',
|
|
offset: 0,
|
|
limit: 100,
|
|
uid: '10000',
|
|
groupId: '54321',
|
|
});
|
|
expect((headers as Record<string, string>)['Content-Type']).toBe('application/json');
|
|
expect((headers as Record<string, string>).Cookie).toContain('p_skey=PSK');
|
|
expect(isJsonRet).toBe(true);
|
|
expect(isArgJson).toBe(true);
|
|
});
|
|
|
|
it('propagates a transport error instead of swallowing it to []', async () => {
|
|
vi.spyOn(RequestUtil, 'HttpGetJson').mockRejectedValue(
|
|
new Error('Unexpected status code: 403'),
|
|
);
|
|
await expect(getDaySignedListWebAPI(cookies, '1', '1')).rejects.toThrow('403');
|
|
});
|
|
|
|
it('throws when the response has no page (auth/cookie failure), not []', async () => {
|
|
vi.spyOn(RequestUtil, 'HttpGetJson').mockResolvedValue({
|
|
retCode: 0,
|
|
funcCode: 0,
|
|
response: {},
|
|
} satisfies DaySignedListResponse as never);
|
|
await expect(getDaySignedListWebAPI(cookies, '1', '1')).rejects.toThrow(
|
|
'无法获取该群组打卡列表',
|
|
);
|
|
});
|
|
});
|