fix(feishu): feishu connector name validate (#797)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
@@ -10,6 +10,11 @@ const readOpenClawConfigMock = vi.fn();
|
||||
const listAgentsSnapshotMock = vi.fn();
|
||||
const sendJsonMock = vi.fn();
|
||||
const proxyAwareFetchMock = vi.fn();
|
||||
const saveChannelConfigMock = vi.fn();
|
||||
const setChannelDefaultAccountMock = vi.fn();
|
||||
const assignChannelAccountToAgentMock = vi.fn();
|
||||
const clearChannelBindingMock = vi.fn();
|
||||
const parseJsonBodyMock = vi.fn();
|
||||
const testOpenClawConfigDir = join(tmpdir(), 'clawx-tests', 'channel-routes-openclaw');
|
||||
|
||||
vi.mock('@electron/utils/channel-config', () => ({
|
||||
@@ -22,17 +27,17 @@ vi.mock('@electron/utils/channel-config', () => ({
|
||||
listConfiguredChannels: (...args: unknown[]) => listConfiguredChannelsMock(...args),
|
||||
listConfiguredChannelsFromConfig: (...args: unknown[]) => listConfiguredChannelsMock(...args),
|
||||
readOpenClawConfig: (...args: unknown[]) => readOpenClawConfigMock(...args),
|
||||
saveChannelConfig: vi.fn(),
|
||||
setChannelDefaultAccount: vi.fn(),
|
||||
saveChannelConfig: (...args: unknown[]) => saveChannelConfigMock(...args),
|
||||
setChannelDefaultAccount: (...args: unknown[]) => setChannelDefaultAccountMock(...args),
|
||||
setChannelEnabled: vi.fn(),
|
||||
validateChannelConfig: vi.fn(),
|
||||
validateChannelCredentials: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/agent-config', () => ({
|
||||
assignChannelAccountToAgent: vi.fn(),
|
||||
assignChannelAccountToAgent: (...args: unknown[]) => assignChannelAccountToAgentMock(...args),
|
||||
clearAllBindingsForChannel: vi.fn(),
|
||||
clearChannelBinding: vi.fn(),
|
||||
clearChannelBinding: (...args: unknown[]) => clearChannelBindingMock(...args),
|
||||
listAgentsSnapshot: (...args: unknown[]) => listAgentsSnapshotMock(...args),
|
||||
listAgentsSnapshotFromConfig: (...args: unknown[]) => listAgentsSnapshotMock(...args),
|
||||
}));
|
||||
@@ -59,7 +64,7 @@ vi.mock('@electron/utils/whatsapp-login', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@electron/api/route-utils', () => ({
|
||||
parseJsonBody: vi.fn().mockResolvedValue({}),
|
||||
parseJsonBody: (...args: unknown[]) => parseJsonBodyMock(...args),
|
||||
sendJson: (...args: unknown[]) => sendJsonMock(...args),
|
||||
}));
|
||||
|
||||
@@ -93,6 +98,8 @@ describe('handleChannelRoutes', () => {
|
||||
vi.resetAllMocks();
|
||||
rmSync(testOpenClawConfigDir, { recursive: true, force: true });
|
||||
proxyAwareFetchMock.mockReset();
|
||||
parseJsonBodyMock.mockResolvedValue({});
|
||||
listConfiguredChannelAccountsMock.mockReturnValue({});
|
||||
listAgentsSnapshotMock.mockResolvedValue({
|
||||
agents: [],
|
||||
channelAccountOwners: {},
|
||||
@@ -194,6 +201,214 @@ describe('handleChannelRoutes', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects non-canonical account ID on channel config save', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
channelType: 'feishu',
|
||||
accountId: '测试账号',
|
||||
config: { appId: 'cli_xxx', appSecret: 'secret' },
|
||||
});
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
const handled = await handleChannelRoutes(
|
||||
{ method: 'POST' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:13210/api/channels/config'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
400,
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: expect.stringContaining('Invalid accountId format'),
|
||||
}),
|
||||
);
|
||||
expect(saveChannelConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows legacy non-canonical account ID on channel config save when account already exists', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
channelType: 'telegram',
|
||||
accountId: 'Legacy_Account',
|
||||
config: { botToken: 'token', allowedUsers: '123456' },
|
||||
});
|
||||
listConfiguredChannelAccountsMock.mockReturnValue({
|
||||
telegram: {
|
||||
defaultAccountId: 'default',
|
||||
accountIds: ['default', 'Legacy_Account'],
|
||||
},
|
||||
});
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
const handled = await handleChannelRoutes(
|
||||
{ method: 'POST' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:13210/api/channels/config'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(saveChannelConfigMock).toHaveBeenCalledWith(
|
||||
'telegram',
|
||||
{ botToken: 'token', allowedUsers: '123456' },
|
||||
'Legacy_Account',
|
||||
);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({ success: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects non-canonical account ID on default-account route', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
channelType: 'feishu',
|
||||
accountId: 'ABC',
|
||||
});
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
await handleChannelRoutes(
|
||||
{ method: 'PUT' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:13210/api/channels/default-account'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
400,
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: expect.stringContaining('Invalid accountId format'),
|
||||
}),
|
||||
);
|
||||
expect(setChannelDefaultAccountMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects non-canonical account ID on binding routes', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
channelType: 'feishu',
|
||||
accountId: 'Account-Upper',
|
||||
agentId: 'main',
|
||||
});
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
await handleChannelRoutes(
|
||||
{ method: 'PUT' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:13210/api/channels/binding'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
400,
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: expect.stringContaining('Invalid accountId format'),
|
||||
}),
|
||||
);
|
||||
expect(assignChannelAccountToAgentMock).not.toHaveBeenCalled();
|
||||
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
channelType: 'feishu',
|
||||
accountId: 'INVALID VALUE',
|
||||
});
|
||||
await handleChannelRoutes(
|
||||
{ method: 'DELETE' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:13210/api/channels/binding'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
expect(clearChannelBindingMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows legacy non-canonical account ID on default-account and binding routes', async () => {
|
||||
listConfiguredChannelAccountsMock.mockReturnValue({
|
||||
feishu: {
|
||||
defaultAccountId: 'default',
|
||||
accountIds: ['default', 'Legacy_Account'],
|
||||
},
|
||||
});
|
||||
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
channelType: 'feishu',
|
||||
accountId: 'Legacy_Account',
|
||||
});
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
await handleChannelRoutes(
|
||||
{ method: 'PUT' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:13210/api/channels/default-account'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
expect(setChannelDefaultAccountMock).toHaveBeenCalledWith('feishu', 'Legacy_Account');
|
||||
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
channelType: 'feishu',
|
||||
accountId: 'Legacy_Account',
|
||||
agentId: 'main',
|
||||
});
|
||||
await handleChannelRoutes(
|
||||
{ method: 'PUT' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:13210/api/channels/binding'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
expect(assignChannelAccountToAgentMock).toHaveBeenCalledWith('main', 'feishu', 'Legacy_Account');
|
||||
});
|
||||
|
||||
it('keeps channel connected when one account is healthy and another errors', async () => {
|
||||
listConfiguredChannelsMock.mockResolvedValue(['telegram']);
|
||||
listConfiguredChannelAccountsMock.mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user