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
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { sendPrivateMessage } from '../src/modules/message-actions';
|
|
import { TempSessionStore, TEMP_SESSION_TTL_MS } from '../src/temp-session-store';
|
|
import type { OneBotInstanceContext } from '../src/instance-context';
|
|
|
|
// The passive gate lives at the very top of sendPrivateMessage, before any
|
|
// message parsing or network call. These tests exercise that gate directly:
|
|
// a temp reply (tempGroupId set) into a session the peer never opened MUST be
|
|
// refused, without ever reaching the send path. This is the structural
|
|
// guarantee that the public build cannot initiate a temp session.
|
|
|
|
function refWith(store: TempSessionStore): OneBotInstanceContext {
|
|
const sendGroupTempMessage = vi.fn();
|
|
const sendPrivate = vi.fn();
|
|
return {
|
|
tempSessions: store,
|
|
// If the gate ever let a send through, these would be called — the tests
|
|
// assert they are NOT for the refused case.
|
|
bridge: { apis: { message: { sendGroupTempMessage, sendPrivate } } },
|
|
} as unknown as OneBotInstanceContext;
|
|
}
|
|
|
|
describe('sendPrivateMessage temp-session gate (passive-only)', () => {
|
|
it.each(['missing', 'other-user', 'other-group', 'expired'])(
|
|
'refuses an image before parsing or uploading when the session is %s',
|
|
async (state) => {
|
|
const store = new TempSessionStore();
|
|
|
|
if (state === 'other-user') {
|
|
store.record(1002, 700);
|
|
}
|
|
|
|
if (state === 'other-group') {
|
|
store.record(1001, 701);
|
|
}
|
|
|
|
if (state === 'expired') {
|
|
store.record(1001, 700, Date.now() - TEMP_SESSION_TTL_MS);
|
|
}
|
|
|
|
const record = vi.spyOn(store, 'record');
|
|
const ref = refWith(store);
|
|
|
|
await expect(
|
|
sendPrivateMessage(
|
|
ref,
|
|
1001,
|
|
[{ type: 'image', data: { file: 'https://must-not-fetch.invalid/image.png' } }],
|
|
false,
|
|
700,
|
|
),
|
|
).rejects.toThrow('no such temp session');
|
|
|
|
expect(ref.bridge.apis.message.sendGroupTempMessage).not.toHaveBeenCalled();
|
|
expect(ref.bridge.apis.message.sendPrivate).not.toHaveBeenCalled();
|
|
expect(record).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it('refuses a temp reply to an unrecorded (user, group) and never sends', async () => {
|
|
const store = new TempSessionStore();
|
|
const ref = refWith(store);
|
|
await expect(
|
|
sendPrivateMessage(
|
|
ref,
|
|
1001,
|
|
[{ type: 'text', data: { text: 'hi' } }],
|
|
false,
|
|
/* tempGroupId */ 700,
|
|
),
|
|
).rejects.toThrow(/passive-only|temp session/i);
|
|
// The gate threw before any wire call.
|
|
expect(
|
|
(ref.bridge.apis.message as { sendGroupTempMessage: ReturnType<typeof vi.fn> })
|
|
.sendGroupTempMessage,
|
|
).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('refuses when a DIFFERENT group is recorded (exact (user, group) match required)', async () => {
|
|
const store = new TempSessionStore();
|
|
store.record(1001, 701); // user opened a session from group 701, not 700
|
|
const ref = refWith(store);
|
|
await expect(
|
|
sendPrivateMessage(ref, 1001, [{ type: 'text', data: { text: 'hi' } }], false, 700),
|
|
).rejects.toThrow(/passive-only|temp session/i);
|
|
});
|
|
|
|
it('rejects text + file before sending a partial temp-session message', async () => {
|
|
const store = new TempSessionStore();
|
|
store.record(1001, 700);
|
|
const ref = refWith(store);
|
|
|
|
await expect(
|
|
sendPrivateMessage(
|
|
ref,
|
|
1001,
|
|
[
|
|
{ type: 'text', data: { text: 'must not be sent' } },
|
|
{ type: 'file', data: { file_id: 'friend-file-id' } },
|
|
],
|
|
false,
|
|
700,
|
|
),
|
|
).rejects.toMatchObject({
|
|
code: 'UNSENDABLE_TYPE',
|
|
elementType: 'file',
|
|
});
|
|
|
|
expect(
|
|
(ref.bridge.apis.message as { sendGroupTempMessage: ReturnType<typeof vi.fn> })
|
|
.sendGroupTempMessage,
|
|
).not.toHaveBeenCalled();
|
|
});
|
|
});
|