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

@@ -4,6 +4,9 @@ import { Channels } from '@/pages/Channels/index';
const hostApiFetchMock = vi.fn();
const subscribeHostEventMock = vi.fn();
const toastSuccessMock = vi.fn();
const toastErrorMock = vi.fn();
const toastWarningMock = vi.fn();
const { gatewayState } = vi.hoisted(() => ({
gatewayState: {
@@ -31,9 +34,9 @@ vi.mock('react-i18next', () => ({
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
success: (...args: unknown[]) => toastSuccessMock(...args),
error: (...args: unknown[]) => toastErrorMock(...args),
warning: (...args: unknown[]) => toastWarningMock(...args),
},
}));
@@ -83,6 +86,94 @@ describe('Channels page status refresh', () => {
});
});
it('blocks saving when custom account ID is non-canonical', async () => {
subscribeHostEventMock.mockImplementation(() => vi.fn());
hostApiFetchMock.mockImplementation(async (path: string) => {
if (path === '/api/channels/accounts') {
return {
success: true,
channels: [
{
channelType: 'feishu',
defaultAccountId: 'default',
status: 'connected',
accounts: [
{
accountId: 'default',
name: 'Primary Account',
configured: true,
status: 'connected',
isDefault: true,
},
],
},
],
};
}
if (path === '/api/agents') {
return {
success: true,
agents: [],
};
}
if (path === '/api/channels/credentials/validate') {
return {
success: true,
valid: true,
warnings: [],
};
}
if (path === '/api/channels/config') {
return {
success: true,
};
}
throw new Error(`Unexpected host API path: ${path}`);
});
render(<Channels />);
await waitFor(() => {
expect(screen.getByText('Feishu / Lark')).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: 'account.add' }));
await waitFor(() => {
expect(screen.getByText('dialog.configureTitle')).toBeInTheDocument();
});
fireEvent.change(screen.getByLabelText('account.customIdLabel'), {
target: { value: '测试账号' },
});
const appIdInput = document.getElementById('appId') as HTMLInputElement | null;
const appSecretInput = document.getElementById('appSecret') as HTMLInputElement | null;
expect(appIdInput).not.toBeNull();
expect(appSecretInput).not.toBeNull();
fireEvent.change(appIdInput!, { target: { value: 'cli_test' } });
fireEvent.change(appSecretInput!, { target: { value: 'secret_test' } });
fireEvent.click(screen.getByRole('button', { name: 'dialog.saveAndConnect' }));
await waitFor(() => {
expect(screen.getByText('account.invalidCanonicalId')).toBeInTheDocument();
});
expect(toastErrorMock).toHaveBeenCalledWith('account.invalidCanonicalId');
const saveCalls = hostApiFetchMock.mock.calls.filter(([path, init]) => (
path === '/api/channels/config'
&& typeof init === 'object'
&& init != null
&& 'method' in init
&& (init as { method?: string }).method === 'POST'
));
expect(saveCalls).toHaveLength(0);
});
it('refetches channel accounts when gateway channel-status events arrive', async () => {
let channelStatusHandler: (() => void) | undefined;
subscribeHostEventMock.mockImplementation((eventName: string, handler: () => void) => {