MM-63912 - enhance validation for channel banner colors (#30981)
* MM-63912 - enhance validation for channel banner colors * fix tests * fix unit tests
Этот коммит содержится в:
@@ -172,7 +172,7 @@ func TestCreateChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: model.NewPointer(true),
|
Enabled: model.NewPointer(true),
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ func TestCreateChannel(t *testing.T) {
|
|||||||
|
|
||||||
require.True(t, *createdChannel.BannerInfo.Enabled)
|
require.True(t, *createdChannel.BannerInfo.Enabled)
|
||||||
require.Equal(t, "banner text", *createdChannel.BannerInfo.Text)
|
require.Equal(t, "banner text", *createdChannel.BannerInfo.Text)
|
||||||
require.Equal(t, "color", *createdChannel.BannerInfo.BackgroundColor)
|
require.Equal(t, "#dddddd", *createdChannel.BannerInfo.BackgroundColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Cannot create channel with banner enabled but not configured", func(t *testing.T) {
|
t.Run("Cannot create channel with banner enabled but not configured", func(t *testing.T) {
|
||||||
@@ -767,7 +767,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: model.NewPointer(true),
|
Enabled: model.NewPointer(true),
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,7 +799,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: model.NewPointer(true),
|
Enabled: model.NewPointer(true),
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -831,7 +831,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: model.NewPointer(true),
|
Enabled: model.NewPointer(true),
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -841,7 +841,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
require.NotNil(t, patchedChannel.BannerInfo)
|
require.NotNil(t, patchedChannel.BannerInfo)
|
||||||
require.True(t, *patchedChannel.BannerInfo.Enabled)
|
require.True(t, *patchedChannel.BannerInfo.Enabled)
|
||||||
require.Equal(t, "banner text", *patchedChannel.BannerInfo.Text)
|
require.Equal(t, "banner text", *patchedChannel.BannerInfo.Text)
|
||||||
require.Equal(t, "color", *patchedChannel.BannerInfo.BackgroundColor)
|
require.Equal(t, "#dddddd", *patchedChannel.BannerInfo.BackgroundColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should not be able to configure channel banner on a channel as a non-admin channel member", func(t *testing.T) {
|
t.Run("Should not be able to configure channel banner on a channel as a non-admin channel member", func(t *testing.T) {
|
||||||
@@ -856,7 +856,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: model.NewPointer(true),
|
Enabled: model.NewPointer(true),
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -877,7 +877,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: model.NewPointer(true),
|
Enabled: model.NewPointer(true),
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,7 +887,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
require.NotNil(t, patchedChannel.BannerInfo)
|
require.NotNil(t, patchedChannel.BannerInfo)
|
||||||
require.True(t, *patchedChannel.BannerInfo.Enabled)
|
require.True(t, *patchedChannel.BannerInfo.Enabled)
|
||||||
require.Equal(t, "banner text", *patchedChannel.BannerInfo.Text)
|
require.Equal(t, "banner text", *patchedChannel.BannerInfo.Text)
|
||||||
require.Equal(t, "color", *patchedChannel.BannerInfo.BackgroundColor)
|
require.Equal(t, "#dddddd", *patchedChannel.BannerInfo.BackgroundColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Cannot enable channel banner without configuring it", func(t *testing.T) {
|
t.Run("Cannot enable channel banner without configuring it", func(t *testing.T) {
|
||||||
@@ -923,7 +923,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: nil,
|
Enabled: nil,
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,7 +933,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
require.NotNil(t, patchedChannel.BannerInfo)
|
require.NotNil(t, patchedChannel.BannerInfo)
|
||||||
require.Nil(t, patchedChannel.BannerInfo.Enabled)
|
require.Nil(t, patchedChannel.BannerInfo.Enabled)
|
||||||
require.Equal(t, "banner text", *patchedChannel.BannerInfo.Text)
|
require.Equal(t, "banner text", *patchedChannel.BannerInfo.Text)
|
||||||
require.Equal(t, "color", *patchedChannel.BannerInfo.BackgroundColor)
|
require.Equal(t, "#dddddd", *patchedChannel.BannerInfo.BackgroundColor)
|
||||||
|
|
||||||
patch = &model.ChannelPatch{
|
patch = &model.ChannelPatch{
|
||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
@@ -947,7 +947,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
require.NotNil(t, patchedChannel.BannerInfo)
|
require.NotNil(t, patchedChannel.BannerInfo)
|
||||||
require.True(t, *patchedChannel.BannerInfo.Enabled)
|
require.True(t, *patchedChannel.BannerInfo.Enabled)
|
||||||
require.Equal(t, "banner text", *patchedChannel.BannerInfo.Text)
|
require.Equal(t, "banner text", *patchedChannel.BannerInfo.Text)
|
||||||
require.Equal(t, "color", *patchedChannel.BannerInfo.BackgroundColor)
|
require.Equal(t, "#dddddd", *patchedChannel.BannerInfo.BackgroundColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Cannot configure channel banner on a DM channel", func(t *testing.T) {
|
t.Run("Cannot configure channel banner on a DM channel", func(t *testing.T) {
|
||||||
@@ -966,7 +966,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: model.NewPointer(true),
|
Enabled: model.NewPointer(true),
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -994,7 +994,7 @@ func TestPatchChannel(t *testing.T) {
|
|||||||
BannerInfo: &model.ChannelBannerInfo{
|
BannerInfo: &model.ChannelBannerInfo{
|
||||||
Enabled: model.NewPointer(true),
|
Enabled: model.NewPointer(true),
|
||||||
Text: model.NewPointer("banner text"),
|
Text: model.NewPointer("banner text"),
|
||||||
BackgroundColor: model.NewPointer("color"),
|
BackgroundColor: model.NewPointer("#dddddd"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8660,6 +8660,10 @@
|
|||||||
"id": "model.channel.is_valid.banner_info.background_color.empty.app_error",
|
"id": "model.channel.is_valid.banner_info.background_color.empty.app_error",
|
||||||
"translation": "Channel banner color cannot be empty when channel banner is enabled"
|
"translation": "Channel banner color cannot be empty when channel banner is enabled"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "model.channel.is_valid.banner_info.background_color.invalid.app_error",
|
||||||
|
"translation": "Channel banner color must be a valid hex color (e.g., #FF0000 or #F00)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "model.channel.is_valid.banner_info.channel_type.app_error",
|
"id": "model.channel.is_valid.banner_info.channel_type.app_error",
|
||||||
"translation": "Channel banner can only be configured on Public and Private channels"
|
"translation": "Channel banner can only be configured on Public and Private channels"
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Validates both 3-digit (#RGB) and 6-digit (#RRGGBB) hex colors
|
||||||
|
channelHexColorRegex = regexp.MustCompile(`^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`)
|
||||||
|
)
|
||||||
|
|
||||||
type ChannelType string
|
type ChannelType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -312,6 +317,10 @@ func (o *Channel) IsValid() *AppError {
|
|||||||
if o.BannerInfo.BackgroundColor == nil || len(*o.BannerInfo.BackgroundColor) == 0 {
|
if o.BannerInfo.BackgroundColor == nil || len(*o.BannerInfo.BackgroundColor) == 0 {
|
||||||
return NewAppError("Channel.IsValid", "model.channel.is_valid.banner_info.background_color.empty.app_error", nil, "", http.StatusBadRequest)
|
return NewAppError("Channel.IsValid", "model.channel.is_valid.banner_info.background_color.empty.app_error", nil, "", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !channelHexColorRegex.MatchString(*o.BannerInfo.BackgroundColor) {
|
||||||
|
return NewAppError("Channel.IsValid", "model.channel.is_valid.banner_info.background_color.invalid.app_error", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -87,6 +87,68 @@ func TestChannelIsValid(t *testing.T) {
|
|||||||
require.NotNil(t, o.IsValid())
|
require.NotNil(t, o.IsValid())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChannelBannerBackgroundColorValidation(t *testing.T) {
|
||||||
|
o := Channel{
|
||||||
|
Id: NewId(),
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
Name: "valid-name",
|
||||||
|
Type: ChannelTypeOpen,
|
||||||
|
Header: "valid-header",
|
||||||
|
Purpose: "valid-purpose",
|
||||||
|
BannerInfo: &ChannelBannerInfo{
|
||||||
|
Enabled: NewPointer(true),
|
||||||
|
Text: NewPointer("Banner Text"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with nil background color
|
||||||
|
o.BannerInfo.BackgroundColor = nil
|
||||||
|
require.NotNil(t, o.IsValid())
|
||||||
|
require.Equal(t, "model.channel.is_valid.banner_info.background_color.empty.app_error", o.IsValid().Id)
|
||||||
|
|
||||||
|
// Test with empty background color
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("")
|
||||||
|
require.NotNil(t, o.IsValid())
|
||||||
|
require.Equal(t, "model.channel.is_valid.banner_info.background_color.empty.app_error", o.IsValid().Id)
|
||||||
|
|
||||||
|
// Test with invalid background color (no # prefix)
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("FF0000")
|
||||||
|
require.NotNil(t, o.IsValid())
|
||||||
|
require.Equal(t, "model.channel.is_valid.banner_info.background_color.invalid.app_error", o.IsValid().Id)
|
||||||
|
|
||||||
|
// Test with invalid background color (invalid characters)
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("#GGGGGG")
|
||||||
|
require.NotNil(t, o.IsValid())
|
||||||
|
require.Equal(t, "model.channel.is_valid.banner_info.background_color.invalid.app_error", o.IsValid().Id)
|
||||||
|
|
||||||
|
// Test with invalid background color (wrong length)
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("#FF00")
|
||||||
|
require.NotNil(t, o.IsValid())
|
||||||
|
require.Equal(t, "model.channel.is_valid.banner_info.background_color.invalid.app_error", o.IsValid().Id)
|
||||||
|
|
||||||
|
// Test with invalid background color (wrong length)
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("#FF00000")
|
||||||
|
require.NotNil(t, o.IsValid())
|
||||||
|
require.Equal(t, "model.channel.is_valid.banner_info.background_color.invalid.app_error", o.IsValid().Id)
|
||||||
|
|
||||||
|
// Test with valid 6-digit hex color
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("#FF0000")
|
||||||
|
require.Nil(t, o.IsValid())
|
||||||
|
|
||||||
|
// Test with valid 6-digit hex color (lowercase)
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("#ff0000")
|
||||||
|
require.Nil(t, o.IsValid())
|
||||||
|
|
||||||
|
// Test with valid 3-digit hex color
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("#F00")
|
||||||
|
require.Nil(t, o.IsValid())
|
||||||
|
|
||||||
|
// Test with valid 3-digit hex color (lowercase)
|
||||||
|
o.BannerInfo.BackgroundColor = NewPointer("#f00")
|
||||||
|
require.Nil(t, o.IsValid())
|
||||||
|
}
|
||||||
|
|
||||||
func TestChannelPreSave(t *testing.T) {
|
func TestChannelPreSave(t *testing.T) {
|
||||||
o := Channel{Name: "test"}
|
o := Channel{Name: "test"}
|
||||||
o.PreSave()
|
o.PreSave()
|
||||||
|
|||||||
@@ -337,4 +337,101 @@ describe('ChannelSettingsConfigurationTab', () => {
|
|||||||
const errorPanel = errorMessage.closest('.SaveChangesPanel');
|
const errorPanel = errorMessage.closest('.SaveChangesPanel');
|
||||||
expect(errorPanel).toHaveClass('error');
|
expect(errorPanel).toHaveClass('error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should save valid colors in hex format', async () => {
|
||||||
|
const {patchChannel} = require('mattermost-redux/actions/channels');
|
||||||
|
patchChannel.mockReturnValue({type: 'MOCK_ACTION', data: {}});
|
||||||
|
|
||||||
|
renderWithContext(<ChannelSettingsConfigurationTab {...baseProps}/>);
|
||||||
|
|
||||||
|
// Enable the banner
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter banner text
|
||||||
|
await act(async () => {
|
||||||
|
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
|
||||||
|
await userEvent.clear(textInput);
|
||||||
|
await userEvent.type(textInput, 'New banner text');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter a valid hex color
|
||||||
|
await act(async () => {
|
||||||
|
const colorInput = screen.getByTestId('color-inputColorValue');
|
||||||
|
await userEvent.clear(colorInput);
|
||||||
|
await userEvent.type(colorInput, '#ff0000');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click the Save button
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify patchChannel was called with the correct color
|
||||||
|
expect(patchChannel).toHaveBeenCalledWith('channel1', expect.objectContaining({
|
||||||
|
banner_info: expect.objectContaining({
|
||||||
|
background_color: expect.stringMatching(/#[0-9a-f]{6}/i), // Match any hex color
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only valid colors will make the save changes panel visible', async () => {
|
||||||
|
const {patchChannel} = require('mattermost-redux/actions/channels');
|
||||||
|
patchChannel.mockReturnValue({type: 'MOCK_ACTION', data: {}});
|
||||||
|
patchChannel.mockClear(); // Clear any previous calls
|
||||||
|
|
||||||
|
const originalColor = '#DDDDDD'; // Original color
|
||||||
|
|
||||||
|
// Create a channel with an valid color
|
||||||
|
const channelWithValidColor = {
|
||||||
|
...mockChannel,
|
||||||
|
banner_info: {
|
||||||
|
enabled: true,
|
||||||
|
text: 'Test text',
|
||||||
|
background_color: originalColor, // Valid color
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render with the invalid color channel
|
||||||
|
renderWithContext(
|
||||||
|
<ChannelSettingsConfigurationTab
|
||||||
|
channel={channelWithValidColor}
|
||||||
|
setAreThereUnsavedChanges={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enter a invalid hex color
|
||||||
|
await act(async () => {
|
||||||
|
const colorInput = screen.getByTestId('color-inputColorValue');
|
||||||
|
await userEvent.clear(colorInput);
|
||||||
|
await userEvent.type(colorInput, 'not-a-color');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do another action to trigger blur on this input so color is validated
|
||||||
|
await act(async () => {
|
||||||
|
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
|
||||||
|
await userEvent.clear(textInput);
|
||||||
|
await userEvent.type(textInput, 'Test text');
|
||||||
|
});
|
||||||
|
|
||||||
|
// if invalid, the color automatically returns to the original color
|
||||||
|
expect(screen.getByTestId('color-inputColorValue')).toHaveValue(originalColor);
|
||||||
|
|
||||||
|
// Check that the save changes panel is not visible
|
||||||
|
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Modify the color to a valid one
|
||||||
|
await act(async () => {
|
||||||
|
const colorInput = screen.getByTestId('color-inputColorValue');
|
||||||
|
await userEvent.clear(colorInput);
|
||||||
|
await userEvent.type(colorInput, '#123456');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a small delay to ensure all state updates are processed
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
// Check that the save changes panel is visible
|
||||||
|
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user