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:
Haze
2026-04-08 19:16:15 +08:00
committed by GitHub
Unverified
parent c1e165d48d
commit d03902dd4d
13 changed files with 521 additions and 17 deletions

View File

@@ -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({