mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* Add AppsForm-based InteractiveDialog implementation with feature flag control - Add InteractiveDialogAppsForm feature flag (default enabled) to control migration path - Enhance AppsForm components with backwards compatibility features: - Add onHide prop support for legacy dialog behavior - Add RADIO field type support with proper rendering - Add required field indicators with red asterisk styling - Use FormattedMessage for "(optional)" text internationalization - Create InteractiveDialogAdapter to bridge legacy dialogs to AppsForm: - Convert DialogElement fields to AppField format with proper type mapping - Handle default value conversion for select, radio, and boolean fields - Implement submission adapter to convert between Apps and legacy formats - Support cancel notifications and proper context creation - Update InteractiveDialog container to route between implementations based on feature flag - Add Redux selector for feature flag state management * Fix circular dependency issue with dynamic InteractiveDialog import Replace static import of InteractiveDialog in websocket_actions.jsx with dynamic import to resolve circular dependency chain that was causing test failures in unrelated components. The static import created a dependency chain: websocket_actions → InteractiveDialog → AppsFormContainer → AppsFormComponent → Markdown → AtMention → user group components This affected many tests because websocket_actions is imported by core system components. The dynamic import only loads InteractiveDialog when the dialog event is actually triggered, improving performance and breaking the circular dependency. * Refactor InteractiveDialog to use isolated DialogRouter architecture Move InteractiveDialogAdapter out of the interactive_dialog directory to break circular dependency chain that was causing test failures in unrelated components. **Changes:** - Create new `dialog_router` component with dynamic imports for both legacy InteractiveDialog and AppsForm-based adapter - Move InteractiveDialogAdapter to dialog_router directory to isolate it from existing components - Update adapter to use dynamic import for AppsFormContainer to avoid circular dependency - Replace embedded routing logic in interactive_dialog/index.tsx with clean DialogRouter usage **Benefits:** - Fixes circular dependency: websocket_actions → InteractiveDialog → AppsFormContainer → AppsFormComponent → Markdown → AtMention components - Cleaner separation of concerns - new code is isolated from existing stable code - Dynamic imports improve performance by loading components only when needed - Maintains backward compatibility while enabling new AppsForm features * lint fixes * Fix TypeScript compilation error in dropdown_input_hybrid Explicitly constrain react-select types to single-select mode (isMulti=false) to resolve type inference conflicts introduced by the InteractiveDialog to AppsForm migration. The component was always single-select only, but the types were previously ambiguous. * Fix ESLint errors in dropdown_input_hybrid - Fix variable naming convention violation - Add eslint-disable comment for intentionally unused components prop - Ensures clean CI/CD pipeline * Enhance InteractiveDialogAdapter with comprehensive validation and type safety - Add enhanced TypeScript interfaces (ValidationError, ConversionContext) - Implement comprehensive dialog and element validation with server-side limits - Add XSS prevention through string sanitization for security - Implement structured logging following Mattermost webapp conventions - Maintain complete backwards compatibility (validation disabled by default) - Add configurable validation modes (validateInputs, strictMode, enableDebugLogging) - Enhance error handling with detailed field-specific validation - Support all dialog element types with proper validation rules - Add proper server-side length limits (title: 24, name: 300, etc.) - Improve type safety throughout conversion logic * fix lint errors * Fix test expectations for XSS sanitization in InteractiveDialogAdapter - Update test assertions to match actual sanitization behavior - Fix expected text content for script and iframe tag removal - Correct event handler sanitization test expectations - All 23 InteractiveDialogAdapter tests now pass successfully * Fix ESLint errors in InteractiveDialogAdapter test file - Replace await-in-loop with Promise.all for boolean conversion tests - Add newline at end of file to satisfy eol-last rule - All tests continue to pass (23/23) * Fix React act() warnings in apps_form_field tests - Wrap async select field renders in act() to prevent console warnings - Fix user, channel, and dynamic select field test warnings - Add proper async/await handling for react-select components - All 17 apps_form_field tests now pass without warnings * Simplify default value handling to match original InteractiveDialog - Remove complex numeric subtype logic - not needed - Use simple `element.default ?? null` for all text/textarea fields - Matches original InteractiveDialog behavior exactly (lines 42-50) - Treat all field types consistently like original dialog - Fix syntax error with missing brace in switch statement * Enhance InteractiveDialogAdapter with server-side error handling and improved type safety - Fix server-side submission failures to keep dialog open and display errors - Add proper TypeScript types for ActionResult<SubmitDialogResponse> - Implement comprehensive error handling for both server and network errors - Add numeric field support with proper number conversion and fallback - Enhance test coverage with server-side error handling scenarios - Maintain backwards compatibility with existing InteractiveDialog behavior * Add internationalization for InteractiveDialogAdapter error messages - Replace hardcoded error strings with proper i18n using intl.formatMessage() - Add new localization keys to server/i18n/en.json for user-facing error messages - Support parameter interpolation for dynamic error details - Maintain backwards compatibility with default English messages - Follow Mattermost internationalization patterns and conventions Error messages localized: - interactive_dialog.submission_failed - interactive_dialog.submission_failed_validation - interactive_dialog.validation_failed - interactive_dialog.element_validation_failed * fix i18n-extract * remove dynamic loading, see if tests still fail * Optimize InteractiveDialogAppsForm validation and performance - Remove redundant validateDialogElement calls (50% validation performance improvement) - Simplify DialogRouter by eliminating unnecessary async loading state - Optimize option validation with combined loop for select/radio fields - Fix TypeScript errors with proper PropsFromRedux type inheritance - Replace regex stringMatching with traditional string patterns in tests - Simplify mocked state in interactive_dialog.test.ts (1500+ lines → minimal) - Fix ESLint issues: trailing spaces and import ordering Performance improvements: - DialogRouter: 50% faster mounting (eliminated loading state) - Validation: 50% fewer validation calls per element - Bundle: No size increase, better tree-shaking * Convert all test files from enzyme to React Testing Library - Replace enzyme shallow/mount with React Testing Library's renderWithContext - Update all assertions to test user-visible behavior instead of implementation details - Remove brittle snapshot test and replace with behavioral assertions - Add comprehensive test coverage for form validation, lookup functionality, and edge cases - Fix all ESLint and styling issues - Remove unused enzyme imports and dependencies This improves test maintainability and aligns with modern React testing best practices by focusing on user interactions rather than component internals. * Fix all failing tests in apps_form_component.test.tsx - Fix error message assertion to match exact text instead of regex - Simplify lookup functionality tests to avoid async rendering issues - Update custom submit buttons test to handle multiple cancel buttons correctly - Remove complex field configurations that were causing React Select warnings - All 27 tests now pass successfully The tests are now more stable and focus on verifying component configuration and user-visible behavior rather than complex async interactions. * fix lint * cleanup tests, fix E2E tests * Improve unit test coverage for InteractiveDialogAdapter and AppsForm components • Add 22 new comprehensive test cases across both components • interactive_dialog_adapter.test.tsx: Added 9 new tests covering advanced validation scenarios, enhanced type conversion, and error handling • apps_form_component.test.tsx: Added 13 new tests covering component lifecycle, field error handling, client-side validation, and lookup functionality • Enhanced coverage includes validation edge cases, error recovery, form state management, and component interaction patterns • All tests passing: 49/49 for interactive_dialog_adapter and 50/50 for apps_form_component * Add submit_label backward compatibility for Interactive Dialog to AppsForm migration This commit restores the submit_label functionality that was lost during the transition from Interactive Dialog to AppsForm. The changes ensure backward compatibility by allowing interactive dialogs to specify custom submit button text through the submit_label property. Changes made: - Added submit_label property to AppForm interface in apps.ts - Updated InteractiveDialogAdapter to extract and pass through submitLabel from legacy dialogs - Modified AppsForm component to use custom submit_label when provided instead of hardcoded "Submit" - Added comprehensive test coverage for the new functionality - Maintained XSS protection through existing sanitization methods * Update e2e tests for AppsForm compatibility and fix TypeScript compilation errors This commit updates interactive dialog e2e tests to work with AppsForm instead of legacy interactive dialog: Key changes: - Update modal selectors from #interactiveDialogModal to #appsModal - Update button selectors from #interactiveDialogSubmit to #appsModalSubmit - Fix label selectors to work with AppsForm DOM structure - Handle ReactSelect portal rendering for dropdown options - Fix TypeScript compilation errors in demo_boolean_spec.ts with triple-slash references - Add ESLint comment spacing fixes to interactive_dialog_adapter.test.tsx - Update checkbox selectors to use generic input[type="checkbox"] instead of element IDs - Remove feature flag disabling InteractiveDialogAppsForm to use AppsForm by default * updates from self review * revert bad file commits * Update files_1_spec.ts * Add DYNAMIC_SELECT support for interactive dialogs Implement comprehensive dynamic select functionality for interactive dialogs by leveraging the Apps framework, enabling real-time option loading via lookup API calls. Server-side changes: - Add DataSourceURL field to DialogElement model - Add DialogSelectOption and LookupDialogResponse types - Add IsValidLookupURL security validation function - Add /api/v4/actions/dialogs/lookup endpoint with permission checks - Add LookupInteractiveDialog app layer method for HTTP requests - Support both dynamic_select type and select with data_source="dynamic" Client-side changes: - Add lookupInteractiveDialog Redux action and Client4 method - Update InteractiveDialogAdapter with full lookup implementation - Add URL resolution priority: data_source_url > call.path > dialog.url - Add client-side URL validation and error handling - Update TypeScript types and test mocks Features: - Real-time option loading as user types in dynamic select fields - Security validation (HTTPS URLs and /plugins/ paths only) - Backward compatible - existing dialogs work unchanged - Two usage patterns supported for flexibility - Graceful error handling with empty results fallback * Add comprehensive unit tests for DYNAMIC_SELECT support This commit adds extensive test coverage for the DYNAMIC_SELECT feature in interactive dialogs, ensuring reliability and maintainability. Server Tests: - API layer tests for /api/v4/actions/dialogs/lookup endpoint - App layer tests for LookupInteractiveDialog functionality - Model validation tests for DialogSelectOption and LookupDialogResponse - URL security validation tests (HTTPS/plugin paths only) - Client library implementation for LookupInteractiveDialog method Webapp Tests: - Interactive dialog adapter tests with 11 comprehensive test cases - Dynamic select element conversion and rendering tests - Lookup API call handling with proper request/response validation - Error handling for failed lookups and network issues - Security testing for URL validation and XSS prevention - Value conversion between dialog and Apps Framework formats - Empty response and edge case handling All tests pass with proper linting and TypeScript compliance. * remove dynamic_select and fix bug * vet, i18n-extract * fix tests * fix lint * fix translations * fix tests * fix tests, allow http:localhost and http:127.0.0.1 * fix tests, shorten display name * initial fixes from reviews * more review cleanup/fixes * i18n-extract * fix interactive dialog tests * fix circular reference error in tests * fix/cleanup tests * lint fix * use makeAsyncComponent instead of DynamicAppsFormContainer * fix tests * fixed missing action * increase tests coverage * lint, styles, test fixes * lint, styles, test fixes * fix tests * mysql fixes * tests fix * Reset cypress.config.ts * fix test * Address review comments for interactive dialog dynamic select - Update minimum server version from 8.0 to 11.0 in API documentation - Add OOM protection using io.LimitReader with 1MB limits for dialog responses - Remove redundant dynamic_select element type validation and tests - Add shared MaxDialogResponseSize constant for consistency * remove blank line * s/bookwork/bullseye to preserve glibc < 2.34 (#33546) With glibc 2.34 and the [removal of libpthread](https://developers.redhat.com/articles/2021/12/17/why-glibc-234-removed-libpthread), binaries built using [Debian bookworm](https://www.debian.org/releases/bookworm/) aren't compatible with older but still supported operating systems like RHEL8. In those environments, Mattermost fails to start with errors like: ``` mattermost/bin/mattermost: /lib64/libc.so.6: version `GLIBC_2.32' not found (required by mattermost/bin/mattermost) mattermost/bin/mattermost: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by mattermost/bin/mattermost) ``` One option might be to generate a static build and avoid the glibc dependency, but this kind of change is out of scope for now. Let's just revert back to using [Debian bullseye](https://www.debian.org/releases/bullseye/), which remains supported until at least August 2026. * quick fix on typo (#33631) * [MM-62991] Ensure extra content is also accounted for in the focus order (#33624) * [MM-65015] Restore Mobile redirection on oauth login (#33626) * Add comprehensive e2e tests for interactive dialog dynamic select feature This commit implements complete end-to-end testing for dynamic select elements in interactive dialogs, including the necessary infrastructure and bug fixes to support the feature. **Key Changes:** - **E2E Test Suite**: Added `dynamic_select_spec.js` with comprehensive test coverage: - UI structure verification and accessibility checks - Dynamic search functionality with real-time filtering - Form submission and validation error handling - Keyboard navigation support - Edge cases (no matches, default values) - **Webhook Infrastructure**: Enhanced test webhook server: - Added `/dynamic_select_dialog_request` and `/dynamic_select_source` endpoints - Implemented role-based search filtering with 12 predefined options - Fixed search parameter handling (`body.submission.query`) - **Dialog Conversion Fix**: Updated `dialog_conversion.ts`: - Added missing `expand: {}` property to lookup objects for dynamic selects - Ensures proper AppCall format for createCallRequest compatibility - **URL Validation Enhancement**: Modified `interactive_dialog_adapter.tsx`: - Allow HTTP localhost URLs for testing scenarios - Maintains security by restricting to localhost/127.0.0.1 only **Test Coverage:** - 7 comprehensive test scenarios covering all dynamic select functionality - Tests validate UI, search, submission, validation, keyboard nav, and accessibility - Proper handling of async operations and React-Select component interactions * fix lint issue * Fix trailing comma in dynamic select webhook response Add trailing comma to items array in onDynamicSelectSource function for consistent JavaScript formatting and better maintainability. * Simplify IsValidLookupURL to follow existing model validation patterns - Changed model-level validation to only check URL format (via IsValidHTTPURL) - Security checks now happen at request time through existing DoActionRequest flow - Aligns with patterns used by Commands, OutgoingWebhooks, and PostActions - Configuration-based security validation (EnableInsecureOutgoingConnections, AllowedUntrustedInternalConnections) applied when lookup requests are made - Updated tests to reflect new validation behavior * fix styles --------- Co-authored-by: Mattermost Build <[email protected]> Co-authored-by: Jesse Hallam <[email protected]> Co-authored-by: sabril <[email protected]> Co-authored-by: Guillermo Vayá <[email protected]> Co-authored-by: Hanzo Dev <[email protected]>
1103 lines
31 KiB
Go
1103 lines
31 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPostAction_IsValid(t *testing.T) {
|
|
tests := map[string]struct {
|
|
action *PostAction
|
|
wantErr string
|
|
}{
|
|
"valid button action with http URL": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "",
|
|
},
|
|
"valid button action with http URL without Id": {
|
|
action: &PostAction{
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "",
|
|
},
|
|
"valid button action with plugin path": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Integration: &PostActionIntegration{
|
|
URL: "/plugins/myplugin/action",
|
|
},
|
|
},
|
|
wantErr: "",
|
|
},
|
|
"valid button action with relative plugin path": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Integration: &PostActionIntegration{
|
|
URL: "plugins/myplugin/action",
|
|
},
|
|
},
|
|
wantErr: "",
|
|
},
|
|
"invalid integration URL": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Integration: &PostActionIntegration{
|
|
URL: "invalid-url",
|
|
},
|
|
},
|
|
wantErr: "action must have an valid integration URL",
|
|
},
|
|
"valid select action with datasource": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Select",
|
|
Type: PostActionTypeSelect,
|
|
DataSource: PostActionDataSourceUsers,
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "",
|
|
},
|
|
"valid select action with options": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Select",
|
|
Type: PostActionTypeSelect,
|
|
Options: []*PostActionOptions{
|
|
{Text: "Opt1", Value: "opt1"},
|
|
},
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "",
|
|
},
|
|
"select action with nil option": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Select",
|
|
Type: PostActionTypeSelect,
|
|
Options: []*PostActionOptions{
|
|
nil,
|
|
{Text: "Opt1", Value: "opt1"},
|
|
},
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "select action contains nil option",
|
|
},
|
|
"missing name": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Type: PostActionTypeButton,
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "action must have a name",
|
|
},
|
|
"invalid style": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Style: "invalid",
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "invalid style 'invalid' - must be one of [default, primary, success, good, warning, danger] or a hex color",
|
|
},
|
|
"valid style": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Style: "primary",
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "",
|
|
},
|
|
"button with options": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Options: []*PostActionOptions{
|
|
{Text: "Opt1", Value: "opt1"},
|
|
},
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "button action must not have options",
|
|
},
|
|
"button with datasource": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
DataSource: PostActionDataSourceUsers,
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "button action must not have a data source",
|
|
},
|
|
"select without datasource or options": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Select",
|
|
Type: PostActionTypeSelect,
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "select action must have either DataSource or Options set",
|
|
},
|
|
"select with both datasource and options": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Select",
|
|
Type: PostActionTypeSelect,
|
|
DataSource: PostActionDataSourceUsers,
|
|
Options: []*PostActionOptions{
|
|
{Text: "Opt1", Value: "opt1"},
|
|
},
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "select action cannot have both DataSource and Options set",
|
|
},
|
|
"invalid datasource": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Select",
|
|
Type: PostActionTypeSelect,
|
|
DataSource: "invalid",
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "invalid data_source 'invalid' for select action",
|
|
},
|
|
"missing integration": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
},
|
|
wantErr: "action must have integration settings",
|
|
},
|
|
"missing integration URL": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Button",
|
|
Type: PostActionTypeButton,
|
|
Integration: &PostActionIntegration{},
|
|
},
|
|
wantErr: "action must have an integration URL",
|
|
},
|
|
"invalid type": {
|
|
action: &PostAction{
|
|
Id: "validid",
|
|
Name: "Test Action",
|
|
Type: "invalid",
|
|
Integration: &PostActionIntegration{
|
|
URL: "http://localhost:8065",
|
|
},
|
|
},
|
|
wantErr: "invalid action type: must be 'button' or 'select'",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
err := tc.action.IsValid()
|
|
if tc.wantErr == "" {
|
|
assert.NoError(t, err, name)
|
|
} else {
|
|
assert.ErrorContains(t, err, tc.wantErr, name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTriggerIdDecodeAndVerification(t *testing.T) {
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("should succeed decoding and validation", func(t *testing.T) {
|
|
userId := NewId()
|
|
clientTriggerId, triggerId, appErr := GenerateTriggerId(userId, key)
|
|
require.Nil(t, appErr)
|
|
decodedClientTriggerId, decodedUserId, appErr := DecodeAndVerifyTriggerId(triggerId, key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
|
assert.Nil(t, appErr)
|
|
assert.Equal(t, clientTriggerId, decodedClientTriggerId)
|
|
assert.Equal(t, userId, decodedUserId)
|
|
})
|
|
|
|
t.Run("should succeed decoding and validation through request structs", func(t *testing.T) {
|
|
actionReq := &PostActionIntegrationRequest{
|
|
UserId: NewId(),
|
|
}
|
|
clientTriggerId, triggerId, appErr := actionReq.GenerateTriggerId(key)
|
|
require.Nil(t, appErr)
|
|
dialogReq := &OpenDialogRequest{TriggerId: triggerId}
|
|
decodedClientTriggerId, decodedUserId, appErr := dialogReq.DecodeAndVerifyTriggerId(key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
|
assert.Nil(t, appErr)
|
|
assert.Equal(t, clientTriggerId, decodedClientTriggerId)
|
|
assert.Equal(t, actionReq.UserId, decodedUserId)
|
|
})
|
|
|
|
t.Run("should fail on base64 decode", func(t *testing.T) {
|
|
_, _, appErr := DecodeAndVerifyTriggerId("junk!", key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "interactive_message.decode_trigger_id.base64_decode_failed", appErr.Id)
|
|
})
|
|
|
|
t.Run("should fail on trigger parsing", func(t *testing.T) {
|
|
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("junk!")), key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "interactive_message.decode_trigger_id.missing_data", appErr.Id)
|
|
})
|
|
|
|
t.Run("should fail on expired timestamp", func(t *testing.T) {
|
|
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:1234567890:junksignature")), key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "interactive_message.decode_trigger_id.expired", appErr.Id)
|
|
})
|
|
|
|
t.Run("should fail on base64 decoding signature", func(t *testing.T) {
|
|
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:12345678900000:junk!")), key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "interactive_message.decode_trigger_id.base64_decode_failed_signature", appErr.Id)
|
|
})
|
|
|
|
t.Run("should fail on bad signature", func(t *testing.T) {
|
|
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:12345678900000:junk")), key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "interactive_message.decode_trigger_id.signature_decode_failed", appErr.Id)
|
|
})
|
|
|
|
t.Run("should fail on bad key", func(t *testing.T) {
|
|
_, triggerId, appErr := GenerateTriggerId(NewId(), key)
|
|
require.Nil(t, appErr)
|
|
newKey, keyErr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, keyErr)
|
|
_, _, appErr = DecodeAndVerifyTriggerId(triggerId, newKey, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "interactive_message.decode_trigger_id.verify_signature_failed", appErr.Id)
|
|
})
|
|
}
|
|
|
|
func TestPostActionIntegrationEquals(t *testing.T) {
|
|
t.Run("equal uncomparable types", func(t *testing.T) {
|
|
pa1 := &PostAction{
|
|
Integration: &PostActionIntegration{
|
|
Context: map[string]any{
|
|
"a": map[string]any{
|
|
"a": 0,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
pa2 := &PostAction{
|
|
Integration: &PostActionIntegration{
|
|
Context: map[string]any{
|
|
"a": map[string]any{
|
|
"a": 0,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
require.True(t, pa1.Equals(pa2))
|
|
})
|
|
|
|
t.Run("equal comparable types", func(t *testing.T) {
|
|
pa1 := &PostAction{
|
|
Integration: &PostActionIntegration{
|
|
Context: map[string]any{
|
|
"a": "test",
|
|
},
|
|
},
|
|
}
|
|
pa2 := &PostAction{
|
|
Integration: &PostActionIntegration{
|
|
Context: map[string]any{
|
|
"a": "test",
|
|
},
|
|
},
|
|
}
|
|
require.True(t, pa1.Equals(pa2))
|
|
})
|
|
|
|
t.Run("non-equal types", func(t *testing.T) {
|
|
pa1 := &PostAction{
|
|
Integration: &PostActionIntegration{
|
|
Context: map[string]any{
|
|
"a": map[string]any{
|
|
"a": 0,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
pa2 := &PostAction{
|
|
Integration: &PostActionIntegration{
|
|
Context: map[string]any{
|
|
"a": "test",
|
|
},
|
|
},
|
|
}
|
|
require.False(t, pa1.Equals(pa2))
|
|
})
|
|
|
|
t.Run("nil check in input integration", func(t *testing.T) {
|
|
pa1 := &PostAction{
|
|
Integration: &PostActionIntegration{},
|
|
}
|
|
|
|
pa2 := &PostAction{
|
|
Integration: nil,
|
|
}
|
|
|
|
require.False(t, pa1.Equals(pa2))
|
|
})
|
|
|
|
t.Run("nil check in original integration", func(t *testing.T) {
|
|
pa1 := &PostAction{
|
|
Integration: nil,
|
|
}
|
|
|
|
pa2 := &PostAction{
|
|
Integration: &PostActionIntegration{},
|
|
}
|
|
|
|
require.False(t, pa1.Equals(pa2))
|
|
})
|
|
|
|
t.Run("both nil", func(t *testing.T) {
|
|
pa1 := &PostAction{
|
|
Integration: nil,
|
|
}
|
|
|
|
pa2 := &PostAction{
|
|
Integration: nil,
|
|
}
|
|
|
|
require.True(t, pa1.Equals(pa2))
|
|
})
|
|
}
|
|
|
|
func TestPostActionOptions_IsValid(t *testing.T) {
|
|
tests := map[string]struct {
|
|
options *PostActionOptions
|
|
wantErr string
|
|
}{
|
|
"valid options": {
|
|
options: &PostActionOptions{
|
|
Text: "Option 1",
|
|
Value: "opt1",
|
|
},
|
|
wantErr: "",
|
|
},
|
|
"missing text": {
|
|
options: &PostActionOptions{
|
|
Value: "opt1",
|
|
},
|
|
wantErr: "text is required",
|
|
},
|
|
"missing value": {
|
|
options: &PostActionOptions{
|
|
Text: "Option 1",
|
|
},
|
|
wantErr: "value is required",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
err := tc.options.IsValid()
|
|
if tc.wantErr == "" {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.ErrorContains(t, err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenDialogRequestIsValid(t *testing.T) {
|
|
getBaseOpenDialogRequest := func() OpenDialogRequest {
|
|
return OpenDialogRequest{
|
|
TriggerId: "triggerId",
|
|
URL: "http://localhost:8065",
|
|
Dialog: Dialog{
|
|
CallbackId: "callbackid",
|
|
Title: "Some Title",
|
|
Elements: []DialogElement{
|
|
{
|
|
DisplayName: "Element Name",
|
|
Name: "element_name",
|
|
Type: "text",
|
|
Placeholder: "Enter a value",
|
|
},
|
|
},
|
|
SubmitLabel: "Submit",
|
|
NotifyOnCancel: false,
|
|
State: "somestate",
|
|
},
|
|
}
|
|
}
|
|
|
|
t.Run("should pass validation", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
err := request.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail on empty url", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.URL = ""
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "empty URL")
|
|
})
|
|
|
|
t.Run("should fail on empty trigger", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.TriggerId = ""
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "empty trigger id")
|
|
})
|
|
|
|
t.Run("should fail on empty dialog title", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Title = ""
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "invalid dialog title")
|
|
})
|
|
|
|
t.Run("should fail on wrong subtype and long dialog title", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements[0].SubType = "wrong SubType"
|
|
request.Dialog.Title = "Very very long Dialog Name"
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "invalid subtype")
|
|
assert.ErrorContains(t, err, "invalid dialog title")
|
|
t.Cleanup(func() {
|
|
request.Dialog.Elements[0].SubType = ""
|
|
request.Dialog.Title = "Some Title"
|
|
})
|
|
})
|
|
|
|
t.Run("should fail on wrong dialog icon url", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.IconURL = "wrong url"
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "invalid icon url")
|
|
})
|
|
|
|
t.Run("should pass on empty dialog icon url", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.IconURL = ""
|
|
err := request.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail on wrong minimal and maximal element length", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements[0].MinLength = 10
|
|
request.Dialog.Elements[0].MaxLength = 9
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "field is not valid")
|
|
assert.ErrorContains(t, err, "min length should be less then max length")
|
|
})
|
|
|
|
t.Run("should fail on wrong element type", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements[0].Type = "wrong type"
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "invalid element type")
|
|
})
|
|
|
|
t.Run("should fail on duplicate element name", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Radio element name",
|
|
Name: "element_name",
|
|
Type: "radio",
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "duplicate dialog element")
|
|
})
|
|
|
|
t.Run("should fail on wrong bool default value", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Bool element name",
|
|
Name: "bool_element_name",
|
|
Type: "bool",
|
|
Default: "wrong default",
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "invalid default of bool")
|
|
})
|
|
|
|
t.Run("should pass on bool default value", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Bool element name",
|
|
Name: "bool_element_name",
|
|
Type: "bool",
|
|
Default: "true",
|
|
})
|
|
err := request.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail on wrong select datasource value", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Select element name",
|
|
Name: "select_element_name",
|
|
Type: "select",
|
|
DataSource: "wrong DataSource",
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "invalid data source")
|
|
})
|
|
|
|
t.Run("should fail on wrong select default value, and not fail with nil dereference", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Select element name",
|
|
Name: "select_element_name",
|
|
Type: "select",
|
|
DataSource: "",
|
|
Default: "default",
|
|
Options: []*PostActionOptions{
|
|
nil,
|
|
},
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "default value \"default\" doesn't exist in options")
|
|
})
|
|
|
|
t.Run("should fail on wrong radio default value", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Radio element name",
|
|
Name: "radio_element_name",
|
|
Type: "radio",
|
|
Default: "default",
|
|
Options: []*PostActionOptions{
|
|
{
|
|
Text: "Text 1",
|
|
Value: "value 1",
|
|
},
|
|
},
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "default value \"default\" doesn't exist in options")
|
|
})
|
|
t.Run("should fail on wrong radio default value, and not fail with nil dereference", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Radio element name",
|
|
Name: "radio_element_name",
|
|
Type: "radio",
|
|
Default: "default",
|
|
Options: []*PostActionOptions{
|
|
nil,
|
|
},
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "default value \"default\" doesn't exist in options")
|
|
})
|
|
t.Run("should pass radio default value", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Radio element name",
|
|
Name: "radio_element_name",
|
|
Type: "radio",
|
|
Default: "default",
|
|
Options: []*PostActionOptions{
|
|
{
|
|
Text: "Text 1",
|
|
Value: "value 1",
|
|
},
|
|
{
|
|
Text: "Text 2",
|
|
Value: "default",
|
|
},
|
|
},
|
|
})
|
|
err := request.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail on too long text placeholder", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements[0].Placeholder = strings.Repeat("x", 151)
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "Placeholder cannot be longer than 150 characters")
|
|
})
|
|
|
|
t.Run("should pass with select element with dynamic data_source", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Dynamic data_source",
|
|
Name: "dynamic_field",
|
|
Type: "select",
|
|
DataSource: "dynamic",
|
|
DataSourceURL: "https://example.com/api/options",
|
|
})
|
|
err := request.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail dynamic data_source without data_source_url", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Dynamic data_source",
|
|
Name: "dynamic_field",
|
|
Type: "select",
|
|
DataSource: "dynamic",
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "dynamic data_source requires data_source_url")
|
|
})
|
|
|
|
t.Run("should fail dynamic data_source with malformed URL", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Dynamic data_source",
|
|
Name: "dynamic_field",
|
|
Type: "select",
|
|
DataSource: "dynamic",
|
|
DataSourceURL: "not-a-valid-url",
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "invalid data_source_url for dynamic select")
|
|
})
|
|
|
|
t.Run("should pass dynamic data_source with HTTP URL", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Dynamic data_source",
|
|
Name: "dynamic_field",
|
|
Type: "select",
|
|
DataSource: "dynamic",
|
|
DataSourceURL: "http://example.com/api/options",
|
|
})
|
|
err := request.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should pass dynamic data_source with plugin URL", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Dynamic data_source",
|
|
Name: "dynamic_field",
|
|
Type: "select",
|
|
DataSource: "dynamic",
|
|
DataSourceURL: "/plugins/myplugin/api/options",
|
|
})
|
|
err := request.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail dynamic data_source with static options", func(t *testing.T) {
|
|
request := getBaseOpenDialogRequest()
|
|
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
|
DisplayName: "Dynamic data_source",
|
|
Name: "dynamic_field",
|
|
Type: "select",
|
|
DataSource: "dynamic",
|
|
DataSourceURL: "https://example.com/api/options",
|
|
Options: []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
},
|
|
})
|
|
err := request.IsValid()
|
|
assert.ErrorContains(t, err, "dynamic select element should not have static options")
|
|
})
|
|
}
|
|
|
|
func TestIsValidLookupURL(t *testing.T) {
|
|
tests := map[string]struct {
|
|
url string
|
|
expected bool
|
|
}{
|
|
"valid HTTPS URL": {
|
|
url: "https://example.com/api/lookup",
|
|
expected: true,
|
|
},
|
|
"valid HTTP URL": {
|
|
url: "http://example.com/api/lookup",
|
|
expected: true,
|
|
},
|
|
"valid plugin path": {
|
|
url: "/plugins/myplugin/lookup",
|
|
expected: true,
|
|
},
|
|
"empty URL": {
|
|
url: "",
|
|
expected: false,
|
|
},
|
|
"path traversal attack": {
|
|
url: "/plugins/../../../etc/passwd",
|
|
expected: false,
|
|
},
|
|
"double slash in plugin path": {
|
|
url: "/plugins//myplugin/lookup",
|
|
expected: false,
|
|
},
|
|
"invalid scheme": {
|
|
url: "ftp://example.com/lookup",
|
|
expected: false,
|
|
},
|
|
"relative path": {
|
|
url: "relative/path",
|
|
expected: false,
|
|
},
|
|
"localhost HTTPS": {
|
|
url: "https://localhost:8080/api/lookup",
|
|
expected: true,
|
|
},
|
|
"localhost HTTP": {
|
|
url: "http://localhost:8080/api/lookup",
|
|
expected: true,
|
|
},
|
|
"127.0.0.1 HTTP": {
|
|
url: "http://127.0.0.1:8080/api/lookup",
|
|
expected: true,
|
|
},
|
|
"malformed URL": {
|
|
url: "not-a-url",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
result := IsValidLookupURL(tc.url)
|
|
assert.Equal(t, tc.expected, result, "IsValidLookupURL(%q) = %v, want %v", tc.url, result, tc.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDialogSelectOption(t *testing.T) {
|
|
t.Run("should create valid option", func(t *testing.T) {
|
|
option := DialogSelectOption{
|
|
Text: "Test Option",
|
|
Value: "test_value",
|
|
}
|
|
assert.Equal(t, "Test Option", option.Text)
|
|
assert.Equal(t, "test_value", option.Value)
|
|
})
|
|
|
|
t.Run("should handle empty values", func(t *testing.T) {
|
|
option := DialogSelectOption{
|
|
Text: "",
|
|
Value: "",
|
|
}
|
|
assert.Equal(t, "", option.Text)
|
|
assert.Equal(t, "", option.Value)
|
|
})
|
|
}
|
|
|
|
func TestLookupDialogResponse(t *testing.T) {
|
|
t.Run("should create valid response", func(t *testing.T) {
|
|
response := LookupDialogResponse{
|
|
Items: []DialogSelectOption{
|
|
{Text: "Option 1", Value: "value1"},
|
|
{Text: "Option 2", Value: "value2"},
|
|
},
|
|
}
|
|
assert.Len(t, response.Items, 2)
|
|
assert.Equal(t, "Option 1", response.Items[0].Text)
|
|
assert.Equal(t, "value1", response.Items[0].Value)
|
|
})
|
|
|
|
t.Run("should handle empty response", func(t *testing.T) {
|
|
response := LookupDialogResponse{
|
|
Items: []DialogSelectOption{},
|
|
}
|
|
assert.Empty(t, response.Items)
|
|
})
|
|
|
|
t.Run("should handle nil items", func(t *testing.T) {
|
|
response := LookupDialogResponse{}
|
|
assert.Nil(t, response.Items)
|
|
})
|
|
}
|
|
|
|
func TestDialogElementMultiSelectValidation(t *testing.T) {
|
|
t.Run("should pass with multiselect on select element", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Multi Select Element",
|
|
Name: "multi_select",
|
|
Type: "select",
|
|
MultiSelect: true,
|
|
Options: []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
{Text: "Option 2", Value: "opt2"},
|
|
},
|
|
}
|
|
err := element.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail with multiselect on non-select element", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Text Element",
|
|
Name: "text_element",
|
|
Type: "text",
|
|
MultiSelect: true,
|
|
}
|
|
err := element.IsValid()
|
|
assert.ErrorContains(t, err, "multiselect can only be used with select elements, got type \"text\"")
|
|
})
|
|
|
|
t.Run("should fail with multiselect on radio element", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Radio Element",
|
|
Name: "radio_element",
|
|
Type: "radio",
|
|
MultiSelect: true,
|
|
Options: []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
},
|
|
}
|
|
err := element.IsValid()
|
|
assert.ErrorContains(t, err, "multiselect can only be used with select elements, got type \"radio\"")
|
|
})
|
|
|
|
t.Run("should fail with multiselect on bool element", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Bool Element",
|
|
Name: "bool_element",
|
|
Type: "bool",
|
|
MultiSelect: true,
|
|
}
|
|
err := element.IsValid()
|
|
assert.ErrorContains(t, err, "multiselect can only be used with select elements, got type \"bool\"")
|
|
})
|
|
|
|
t.Run("should pass with multiselect and valid comma-separated defaults", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Multi Select Element",
|
|
Name: "multi_select",
|
|
Type: "select",
|
|
MultiSelect: true,
|
|
Default: "opt1,opt2",
|
|
Options: []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
{Text: "Option 2", Value: "opt2"},
|
|
{Text: "Option 3", Value: "opt3"},
|
|
},
|
|
}
|
|
err := element.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should pass with multiselect and valid spaced comma-separated defaults", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Multi Select Element",
|
|
Name: "multi_select",
|
|
Type: "select",
|
|
MultiSelect: true,
|
|
Default: "opt1, opt2, opt3",
|
|
Options: []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
{Text: "Option 2", Value: "opt2"},
|
|
{Text: "Option 3", Value: "opt3"},
|
|
},
|
|
}
|
|
err := element.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail with multiselect and invalid default value not in options", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Multi Select Element",
|
|
Name: "multi_select",
|
|
Type: "select",
|
|
MultiSelect: true,
|
|
Default: "opt1,invalid_opt",
|
|
Options: []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
{Text: "Option 2", Value: "opt2"},
|
|
},
|
|
}
|
|
err := element.IsValid()
|
|
assert.ErrorContains(t, err, "multiselect default value \"opt1,invalid_opt\" contains values not in options")
|
|
})
|
|
|
|
t.Run("should pass with multiselect and empty default", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Multi Select Element",
|
|
Name: "multi_select",
|
|
Type: "select",
|
|
MultiSelect: true,
|
|
Default: "",
|
|
Options: []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
{Text: "Option 2", Value: "opt2"},
|
|
},
|
|
}
|
|
err := element.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should pass with multiselect and data source", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Multi Select Element",
|
|
Name: "multi_select",
|
|
Type: "select",
|
|
MultiSelect: true,
|
|
DataSource: "users",
|
|
}
|
|
err := element.IsValid()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should fail with single-select and comma-separated default values", func(t *testing.T) {
|
|
element := &DialogElement{
|
|
DisplayName: "Single Select Element",
|
|
Name: "single_select",
|
|
Type: "select",
|
|
MultiSelect: false,
|
|
Default: "opt1,opt2",
|
|
Options: []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
{Text: "Option 2", Value: "opt2"},
|
|
},
|
|
}
|
|
err := element.IsValid()
|
|
assert.ErrorContains(t, err, "default value \"opt1,opt2\" doesn't exist in options")
|
|
})
|
|
}
|
|
|
|
func TestIsMultiSelectDefaultInOptions(t *testing.T) {
|
|
options := []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
{Text: "Option 2", Value: "opt2"},
|
|
{Text: "Option 3", Value: "opt3"},
|
|
{Text: "Option 4", Value: "opt4"},
|
|
}
|
|
|
|
t.Run("should return true for empty default", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("", options)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should return true for single valid default", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("opt1", options)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should return true for multiple valid defaults", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("opt1,opt2", options)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should return true for multiple valid defaults with spaces", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("opt1, opt2, opt3", options)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should return true for all valid defaults", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("opt1,opt2,opt3,opt4", options)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should return false for single invalid default", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("invalid", options)
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("should return false for mixed valid and invalid defaults", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("opt1,invalid,opt2", options)
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("should return false for all invalid defaults", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("invalid1,invalid2", options)
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("should handle empty values in comma-separated string", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("opt1,,opt2", options)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should handle only commas", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions(",,", options)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should return true for single comma", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions(",", options)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should handle nil options", func(t *testing.T) {
|
|
result := isMultiSelectDefaultInOptions("opt1", nil)
|
|
assert.False(t, result)
|
|
})
|
|
|
|
t.Run("should handle options with nil entries", func(t *testing.T) {
|
|
optionsWithNil := []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
nil,
|
|
{Text: "Option 2", Value: "opt2"},
|
|
}
|
|
result := isMultiSelectDefaultInOptions("opt1,opt2", optionsWithNil)
|
|
assert.True(t, result)
|
|
})
|
|
|
|
t.Run("should return false when value matches nil option", func(t *testing.T) {
|
|
optionsWithNil := []*PostActionOptions{
|
|
{Text: "Option 1", Value: "opt1"},
|
|
nil,
|
|
}
|
|
result := isMultiSelectDefaultInOptions("opt1,opt2", optionsWithNil)
|
|
assert.False(t, result)
|
|
})
|
|
}
|