feat: enhance channel configuration UI and validation
- Updated ChannelInstructionsPanel to include a button for viewing documentation, improving user guidance. - Enhanced ChannelTokenField to support showing/hiding secret values with appropriate labels and icons. - Refined ChannelTypeSelector to display connection type icons and improved layout for better user experience. - Added new messages for documentation links, validation feedback, and secret management in i18n. - Extended ChannelMeta to include optional documentation URLs for better context on configuration fields. - Implemented credential validation logic in ChannelsPage to ensure user inputs are validated before saving. - Introduced ChannelLogo component to display channel icons in the UI. - Added tests for channel credential validation to ensure proper error handling and feedback.
This commit is contained in:
@@ -4,6 +4,15 @@ const mocks = vi.hoisted(() => ({
|
||||
configGet: vi.fn(),
|
||||
listStoredChannelAccountRecords: vi.fn(),
|
||||
listCronJobs: vi.fn(),
|
||||
listAgentsSnapshot: vi.fn(),
|
||||
assignChannelToAgent: vi.fn(),
|
||||
clearAllChannelBindings: vi.fn(),
|
||||
clearChannelBinding: vi.fn(),
|
||||
listAgentsSnapshotResult: {
|
||||
agents: [],
|
||||
channelOwners: {},
|
||||
channelAccountOwners: {},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@electron/service/config-service', () => ({
|
||||
@@ -23,17 +32,46 @@ vi.mock('../electron/utils/channel-config', () => ({
|
||||
listStoredChannelAccountRecords: mocks.listStoredChannelAccountRecords,
|
||||
}));
|
||||
|
||||
vi.mock('../electron/utils/agent-config', () => ({
|
||||
assignChannelToAgent: mocks.assignChannelToAgent,
|
||||
clearAllChannelBindings: mocks.clearAllChannelBindings,
|
||||
clearChannelBinding: mocks.clearChannelBinding,
|
||||
listAgentsSnapshot: mocks.listAgentsSnapshot,
|
||||
}));
|
||||
|
||||
import {
|
||||
getSelectedChannelsConfig,
|
||||
listSelectedChannelAccountGroups,
|
||||
listSelectedChannelTargets,
|
||||
} from '../electron/utils/channels';
|
||||
import { normalizeRequest } from '../electron/api/route-utils';
|
||||
import { handleChannelRoutes } from '../electron/api/routes/channels';
|
||||
|
||||
function createRouteContext() {
|
||||
const notifyRuntimeChanged = vi.fn();
|
||||
return {
|
||||
gatewayManager: {
|
||||
notifyRuntimeChanged,
|
||||
},
|
||||
providerApiService: {
|
||||
getAccounts: () => [],
|
||||
getDefault: () => ({ accountId: 'default' }),
|
||||
},
|
||||
mainWindow: null,
|
||||
notifyRuntimeChanged,
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('channels utilities', () => {
|
||||
beforeEach(() => {
|
||||
mocks.configGet.mockReset();
|
||||
mocks.listStoredChannelAccountRecords.mockReset();
|
||||
mocks.listCronJobs.mockReset();
|
||||
mocks.listAgentsSnapshot.mockReset();
|
||||
mocks.assignChannelToAgent.mockReset();
|
||||
mocks.clearAllChannelBindings.mockReset();
|
||||
mocks.clearChannelBinding.mockReset();
|
||||
mocks.listAgentsSnapshot.mockReturnValue(mocks.listAgentsSnapshotResult);
|
||||
});
|
||||
|
||||
it('normalizes and groups selected channels by inferred channel type', () => {
|
||||
@@ -285,3 +323,118 @@ describe('channels utilities', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('channel credential validation route', () => {
|
||||
it('returns a valid result with warnings for a token/password/url payload', async () => {
|
||||
const ctx = createRouteContext();
|
||||
|
||||
const response = await handleChannelRoutes(normalizeRequest({
|
||||
path: '/api/channels/credentials/validate',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
channelType: 'imessage',
|
||||
accountId: 'acct-1',
|
||||
credentials: {
|
||||
serverUrl: 'http://bridge.example.com',
|
||||
password: 'bridge-password',
|
||||
},
|
||||
}),
|
||||
}), ctx);
|
||||
|
||||
expect(response?.ok).toBe(true);
|
||||
expect(response?.json).toMatchObject({
|
||||
success: true,
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: ['Server URL 使用了不安全的 http URL,建议改用 https'],
|
||||
details: expect.objectContaining({
|
||||
channelType: 'imessage',
|
||||
accountId: 'acct-1',
|
||||
connectionType: 'token',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response?.json).toMatchObject({
|
||||
details: expect.objectContaining({
|
||||
fields: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: 'serverUrl',
|
||||
kind: 'url',
|
||||
provided: true,
|
||||
valid: true,
|
||||
warnings: ['Server URL 使用了不安全的 http URL,建议改用 https'],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: 'password',
|
||||
kind: 'password',
|
||||
provided: true,
|
||||
valid: true,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(ctx.notifyRuntimeChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns field errors for missing token and text credentials', async () => {
|
||||
const ctx = createRouteContext();
|
||||
|
||||
const response = await handleChannelRoutes(normalizeRequest({
|
||||
path: '/api/channels/credentials/validate',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
channelType: 'discord',
|
||||
credentials: {
|
||||
token: ' ',
|
||||
guildId: '',
|
||||
channelId: '123456789012345678',
|
||||
},
|
||||
}),
|
||||
}), ctx);
|
||||
|
||||
expect(response?.ok).toBe(true);
|
||||
expect(response?.json).toMatchObject({
|
||||
success: true,
|
||||
valid: false,
|
||||
warnings: [],
|
||||
errors: [
|
||||
'Bot Token is required',
|
||||
'Guild ID is required',
|
||||
],
|
||||
details: expect.objectContaining({
|
||||
channelType: 'discord',
|
||||
connectionType: 'token',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response?.json).toMatchObject({
|
||||
details: expect.objectContaining({
|
||||
fields: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: 'token',
|
||||
kind: 'password',
|
||||
provided: false,
|
||||
valid: false,
|
||||
errors: ['Bot Token is required'],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: 'guildId',
|
||||
kind: 'text',
|
||||
provided: false,
|
||||
valid: false,
|
||||
errors: ['Guild ID is required'],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: 'channelId',
|
||||
kind: 'text',
|
||||
provided: true,
|
||||
valid: true,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(ctx.notifyRuntimeChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user