SnowLuma v2.4.2 source snapshot
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
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
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,132 @@
|
||||
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(
|
||||
'无法获取该群组打卡列表',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user