[MM-45272] Fix MM-45272 (#24701)
* Fix MM-45272 * Properly handle permalinks * Fix * Fix tests * Handle only not found case for team member * Fix lint * Use proper config value * Separate permission in several statements * Add tests * Fix lint * Revert changes on utils * Address feedback and more fixes * Address feedback * Fix test * Fix test and related bug * Fix and reorder test * Address feedback * Address feedback --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c395ec6245
Коммит
2ff0fe343e
@@ -3795,7 +3795,7 @@ func TestPostGetInfo(t *testing.T) {
|
|||||||
dmPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: dmChannel.Id})
|
dmPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: dmChannel.Id})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
openTeam, _, err := sysadminClient.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "open-team", DisplayName: "Open Team"})
|
openTeam, _, err := sysadminClient.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "open-team", DisplayName: "Open Team", AllowOpenInvite: true})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
openTeamOpenChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: openTeam.Id, Type: model.ChannelTypeOpen, Name: "open-team-open-channel", DisplayName: "Open Team - Open Channel"})
|
openTeamOpenChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: openTeam.Id, Type: model.ChannelTypeOpen, Name: "open-team-open-channel", DisplayName: "Open Team - Open Channel"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -3803,7 +3803,7 @@ func TestPostGetInfo(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Alt team is a team without the sysadmin in it.
|
// Alt team is a team without the sysadmin in it.
|
||||||
altOpenTeam, _, err := client.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "alt-open-team", DisplayName: "Alt Open Team"})
|
altOpenTeam, _, err := client.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "alt-open-team", DisplayName: "Alt Open Team", AllowOpenInvite: true})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
altOpenTeamOpenChannel, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: altOpenTeam.Id, Type: model.ChannelTypeOpen, Name: "alt-open-team-open-channel", DisplayName: "Open Team - Open Channel"})
|
altOpenTeamOpenChannel, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: altOpenTeam.Id, Type: model.ChannelTypeOpen, Name: "alt-open-team-open-channel", DisplayName: "Open Team - Open Channel"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -4008,8 +4008,12 @@ func TestPostGetInfo(t *testing.T) {
|
|||||||
require.Equal(t, tc.channel.DisplayName, info.ChannelDisplayName)
|
require.Equal(t, tc.channel.DisplayName, info.ChannelDisplayName)
|
||||||
require.Equal(t, tc.hasJoinedChannel, info.HasJoinedChannel)
|
require.Equal(t, tc.hasJoinedChannel, info.HasJoinedChannel)
|
||||||
if tc.team != nil {
|
if tc.team != nil {
|
||||||
|
teamType := "I"
|
||||||
|
if tc.team.AllowOpenInvite {
|
||||||
|
teamType = "O"
|
||||||
|
}
|
||||||
require.Equal(t, tc.team.Id, info.TeamId)
|
require.Equal(t, tc.team.Id, info.TeamId)
|
||||||
require.Equal(t, tc.team.Type, info.TeamType)
|
require.Equal(t, teamType, info.TeamType)
|
||||||
require.Equal(t, tc.team.DisplayName, info.TeamDisplayName)
|
require.Equal(t, tc.team.DisplayName, info.TeamDisplayName)
|
||||||
require.Equal(t, tc.hasJoinedTeam, info.HasJoinedTeam)
|
require.Equal(t, tc.hasJoinedTeam, info.HasJoinedTeam)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -380,5 +380,13 @@ func (a *App) HasPermissionToReadChannel(c request.CTX, userID string, channel *
|
|||||||
if !*a.Config().TeamSettings.ExperimentalViewArchivedChannels && channel.DeleteAt != 0 {
|
if !*a.Config().TeamSettings.ExperimentalViewArchivedChannels && channel.DeleteAt != 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannelContent) || (channel.Type == model.ChannelTypeOpen && a.HasPermissionToTeam(c, userID, channel.TeamId, model.PermissionReadPublicChannel))
|
if a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannelContent) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.Type == model.ChannelTypeOpen && !*a.Config().ComplianceSettings.Enable {
|
||||||
|
return a.HasPermissionToTeam(c, userID, channel.TeamId, model.PermissionReadPublicChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -553,3 +553,117 @@ func TestSessionHasPermissionToGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHasPermissionToReadChannel(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
ttcc := []struct {
|
||||||
|
name string
|
||||||
|
configViewArchived bool
|
||||||
|
configComplianceEnabled bool
|
||||||
|
channelDeleted bool
|
||||||
|
canReadChannel bool
|
||||||
|
channelIsOpen bool
|
||||||
|
canReadPublicChannel bool
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Cannot read archived channels if the config doesn't allow it",
|
||||||
|
configViewArchived: false,
|
||||||
|
configComplianceEnabled: true,
|
||||||
|
channelDeleted: true,
|
||||||
|
canReadChannel: true,
|
||||||
|
channelIsOpen: true,
|
||||||
|
canReadPublicChannel: true,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Can read if it has permissions to read",
|
||||||
|
configViewArchived: false,
|
||||||
|
configComplianceEnabled: true,
|
||||||
|
channelDeleted: false,
|
||||||
|
canReadChannel: true,
|
||||||
|
channelIsOpen: false,
|
||||||
|
canReadPublicChannel: true,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cannot read private channels if it has no permission",
|
||||||
|
configViewArchived: false,
|
||||||
|
configComplianceEnabled: false,
|
||||||
|
channelDeleted: false,
|
||||||
|
canReadChannel: false,
|
||||||
|
channelIsOpen: false,
|
||||||
|
canReadPublicChannel: true,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cannot read open channels if compliance is enabled",
|
||||||
|
configViewArchived: false,
|
||||||
|
configComplianceEnabled: true,
|
||||||
|
channelDeleted: false,
|
||||||
|
canReadChannel: false,
|
||||||
|
channelIsOpen: true,
|
||||||
|
canReadPublicChannel: true,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cannot read open channels if it has no team permissions",
|
||||||
|
configViewArchived: false,
|
||||||
|
configComplianceEnabled: false,
|
||||||
|
channelDeleted: false,
|
||||||
|
canReadChannel: false,
|
||||||
|
channelIsOpen: true,
|
||||||
|
canReadPublicChannel: false,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Can read open channels if it has team permissions and compliance is not enabled",
|
||||||
|
configViewArchived: false,
|
||||||
|
configComplianceEnabled: false,
|
||||||
|
channelDeleted: false,
|
||||||
|
canReadChannel: false,
|
||||||
|
channelIsOpen: true,
|
||||||
|
canReadPublicChannel: true,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range ttcc {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
configViewArchived := tc.configViewArchived
|
||||||
|
configComplianceEnabled := tc.configComplianceEnabled
|
||||||
|
cfg.TeamSettings.ExperimentalViewArchivedChannels = &configViewArchived
|
||||||
|
cfg.ComplianceSettings.Enable = &configComplianceEnabled
|
||||||
|
})
|
||||||
|
|
||||||
|
team := th.CreateTeam()
|
||||||
|
if tc.canReadPublicChannel {
|
||||||
|
th.LinkUserToTeam(th.BasicUser2, team)
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel *model.Channel
|
||||||
|
if tc.channelIsOpen {
|
||||||
|
channel = th.CreateChannel(th.Context, team)
|
||||||
|
} else {
|
||||||
|
channel = th.CreatePrivateChannel(th.Context, team)
|
||||||
|
}
|
||||||
|
if tc.canReadChannel {
|
||||||
|
_, err := th.App.AddUserToChannel(th.Context, th.BasicUser2, channel, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.channelDeleted {
|
||||||
|
err := th.App.DeleteChannel(th.Context, channel, th.SystemAdminUser.Id)
|
||||||
|
require.Nil(t, err)
|
||||||
|
channel, err = th.App.GetChannel(th.Context, channel.Id)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := th.App.HasPermissionToReadChannel(th.Context, th.BasicUser2.Id, channel)
|
||||||
|
require.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -799,10 +799,13 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
originalEmbeds := post.Metadata.Embeds
|
||||||
|
originalProps := post.GetProps()
|
||||||
permalinkPreviewedPost := post.GetPreviewPost()
|
permalinkPreviewedPost := post.GetPreviewPost()
|
||||||
for _, userID := range userIDs {
|
for _, userID := range userIDs {
|
||||||
if permalinkPreviewedPost != nil {
|
if permalinkPreviewedPost != nil {
|
||||||
post.Metadata.Embeds[0].Data = permalinkPreviewedPost
|
post.Metadata.Embeds = originalEmbeds
|
||||||
|
post.SetProps(originalProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
postForUser := a.sanitizePostMetadataForUserAndChannel(c, post, permalinkPreviewedPost, permalinkPreviewedChannel, userID)
|
postForUser := a.sanitizePostMetadataForUserAndChannel(c, post, permalinkPreviewedPost, permalinkPreviewedChannel, userID)
|
||||||
@@ -822,6 +825,12 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
|
|||||||
a.Publish(messageCopy)
|
a.Publish(messageCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore the metadata that may have been removed in the sanitization
|
||||||
|
if permalinkPreviewedPost != nil {
|
||||||
|
post.Metadata.Embeds = originalEmbeds
|
||||||
|
post.SetProps(originalProps)
|
||||||
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2033,7 +2042,7 @@ func (a *App) GetPostIfAuthorized(c request.CTX, postID string, session *model.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !a.SessionHasPermissionToChannel(c, *session, channel.Id, model.PermissionReadChannelContent) {
|
if !a.SessionHasPermissionToChannel(c, *session, channel.Id, model.PermissionReadChannelContent) {
|
||||||
if channel.Type == model.ChannelTypeOpen {
|
if channel.Type == model.ChannelTypeOpen && !*a.Config().ComplianceSettings.Enable {
|
||||||
if !a.SessionHasPermissionToTeam(*session, channel.TeamId, model.PermissionReadPublicChannel) {
|
if !a.SessionHasPermissionToTeam(*session, channel.TeamId, model.PermissionReadPublicChannel) {
|
||||||
return nil, a.MakePermissionError(session, []*model.Permission{model.PermissionReadPublicChannel})
|
return nil, a.MakePermissionError(session, []*model.Permission{model.PermissionReadPublicChannel})
|
||||||
}
|
}
|
||||||
@@ -2233,10 +2242,23 @@ func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model
|
|||||||
return nil, appErr
|
return nil, appErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if team.Type == model.TeamOpen {
|
teamMember, appErr := a.GetTeamMember(c, channel.TeamId, userID)
|
||||||
hasPermissionToAccessTeam = a.HasPermissionToTeam(c, userID, team.Id, model.PermissionJoinPublicTeams)
|
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
|
||||||
} else if team.Type == model.TeamInvite {
|
return nil, appErr
|
||||||
hasPermissionToAccessTeam = a.HasPermissionToTeam(c, userID, team.Id, model.PermissionJoinPrivateTeams)
|
}
|
||||||
|
|
||||||
|
if appErr == nil {
|
||||||
|
if teamMember.DeleteAt == 0 {
|
||||||
|
hasPermissionToAccessTeam = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasPermissionToAccessTeam {
|
||||||
|
if team.AllowOpenInvite {
|
||||||
|
hasPermissionToAccessTeam = a.HasPermissionToTeam(c, userID, team.Id, model.PermissionJoinPublicTeams)
|
||||||
|
} else {
|
||||||
|
hasPermissionToAccessTeam = a.HasPermissionToTeam(c, userID, team.Id, model.PermissionJoinPrivateTeams)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This happens in case of DMs and GMs.
|
// This happens in case of DMs and GMs.
|
||||||
@@ -2269,12 +2291,16 @@ func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model
|
|||||||
HasJoinedChannel: channelMemberErr == nil,
|
HasJoinedChannel: channelMemberErr == nil,
|
||||||
}
|
}
|
||||||
if team != nil {
|
if team != nil {
|
||||||
_, teamMemberErr := a.GetTeamMember(c, team.Id, userID)
|
teamMember, teamMemberErr := a.GetTeamMember(c, team.Id, userID)
|
||||||
|
|
||||||
|
teamType := model.TeamInvite
|
||||||
|
if team.AllowOpenInvite {
|
||||||
|
teamType = model.TeamOpen
|
||||||
|
}
|
||||||
info.TeamId = team.Id
|
info.TeamId = team.Id
|
||||||
info.TeamType = team.Type
|
info.TeamType = teamType
|
||||||
info.TeamDisplayName = team.DisplayName
|
info.TeamDisplayName = team.DisplayName
|
||||||
info.HasJoinedTeam = teamMemberErr == nil
|
info.HasJoinedTeam = teamMemberErr == nil && teamMember.DeleteAt == 0
|
||||||
}
|
}
|
||||||
return &info, nil
|
return &info, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,7 +196,19 @@ func (a *App) sanitizePostMetadataForUserAndChannel(c request.CTX, post *model.P
|
|||||||
}
|
}
|
||||||
|
|
||||||
if previewedChannel != nil && !a.HasPermissionToReadChannel(c, userID, previewedChannel) {
|
if previewedChannel != nil && !a.HasPermissionToReadChannel(c, userID, previewedChannel) {
|
||||||
post.Metadata.Embeds[0].Data = nil
|
// Remove all permalink embeds and only keep non-permalink embeds.
|
||||||
|
// We always have only one permalink embed even if the post
|
||||||
|
// contains multiple permalinks.
|
||||||
|
var newEmbeds []*model.PostEmbed
|
||||||
|
for _, embed := range post.Metadata.Embeds {
|
||||||
|
if embed.Type != model.PostEmbedPermalink {
|
||||||
|
newEmbeds = append(newEmbeds, embed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post.Metadata.Embeds = newEmbeds
|
||||||
|
|
||||||
|
post.DelProp(model.PostPropsPreviewedPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
return post
|
return post
|
||||||
@@ -229,6 +241,8 @@ func (a *App) SanitizePostMetadataForUser(c request.CTX, post *model.Post, userI
|
|||||||
}
|
}
|
||||||
|
|
||||||
post.Metadata.Embeds = newEmbeds
|
post.Metadata.Embeds = newEmbeds
|
||||||
|
|
||||||
|
post.DelProp(model.PostPropsPreviewedPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
return post, nil
|
return post, nil
|
||||||
|
|||||||
@@ -2820,7 +2820,7 @@ func TestSanitizePostMetadataForUserAndChannel(t *testing.T) {
|
|||||||
require.Nil(t, appErr)
|
require.Nil(t, appErr)
|
||||||
|
|
||||||
actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, directChannel, guest.Id)
|
actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, directChannel, guest.Id)
|
||||||
assert.Nil(t, actual.Metadata.Embeds[0].Data)
|
assert.Len(t, actual.Metadata.Embeds, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should not preview for archived channels", func(t *testing.T) {
|
t.Run("should not preview for archived channels", func(t *testing.T) {
|
||||||
@@ -2882,7 +2882,7 @@ func TestSanitizePostMetadataForUserAndChannel(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, publicChannel, th.BasicUser.Id)
|
actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, publicChannel, th.BasicUser.Id)
|
||||||
assert.Nil(t, actual.Metadata.Embeds[0].Data)
|
assert.Len(t, actual.Metadata.Embeds, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1001,51 +1001,49 @@ func TestCreatePost(t *testing.T) {
|
|||||||
directChannel, err := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
|
directChannel, err := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
referencedPost := &model.Post{
|
|
||||||
ChannelId: th.BasicChannel.Id,
|
|
||||||
Message: "hello world",
|
|
||||||
UserId: th.BasicUser.Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
th.Context.Session().UserId = th.BasicUser.Id
|
th.Context.Session().UserId = th.BasicUser.Id
|
||||||
|
|
||||||
referencedPost, err = th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, false, false)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Description string
|
Description string
|
||||||
Channel *model.Channel
|
Channel *model.Channel
|
||||||
Author string
|
Author string
|
||||||
Assert func(t assert.TestingT, object any, msgAndArgs ...any) bool
|
Length int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Description: "removes metadata from post for members who cannot read channel",
|
Description: "removes metadata from post for members who cannot read channel",
|
||||||
Channel: directChannel,
|
Channel: directChannel,
|
||||||
Author: user1.Id,
|
Author: user1.Id,
|
||||||
Assert: assert.Nil,
|
Length: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Description: "does not remove metadata from post for members who can read channel",
|
Description: "does not remove metadata from post for members who can read channel",
|
||||||
Channel: th.BasicChannel,
|
Channel: th.BasicChannel,
|
||||||
Author: th.BasicUser.Id,
|
Author: th.BasicUser.Id,
|
||||||
Assert: assert.NotNil,
|
Length: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.Description, func(t *testing.T) {
|
t.Run(testCase.Description, func(t *testing.T) {
|
||||||
previewPost := &model.Post{
|
referencedPost := &model.Post{
|
||||||
ChannelId: testCase.Channel.Id,
|
ChannelId: testCase.Channel.Id,
|
||||||
Message: permalink,
|
Message: "hello world",
|
||||||
UserId: testCase.Author,
|
UserId: testCase.Author,
|
||||||
}
|
}
|
||||||
|
referencedPost, err = th.App.CreatePost(th.Context, referencedPost, testCase.Channel, false, false)
|
||||||
previewPost, err = th.App.CreatePost(th.Context, previewPost, testCase.Channel, false, false)
|
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
testCase.Assert(t, previewPost.Metadata.Embeds[0].Data)
|
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
||||||
|
previewPost := &model.Post{
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
Message: permalink,
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
previewPost, err = th.App.CreatePost(th.Context, previewPost, th.BasicChannel, false, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Len(t, previewPost.Metadata.Embeds, testCase.Length)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1451,73 +1449,6 @@ func TestUpdatePost(t *testing.T) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
assert.Equal(t, testPost.GetProps(), model.StringInterface{"previewed_post": referencedPost.Id})
|
assert.Equal(t, testPost.GetProps(), model.StringInterface{"previewed_post": referencedPost.Id})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("sanitizes post metadata appropriately", func(t *testing.T) {
|
|
||||||
th := Setup(t).InitBasic()
|
|
||||||
defer th.TearDown()
|
|
||||||
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
||||||
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
||||||
})
|
|
||||||
|
|
||||||
th.AddUserToChannel(th.BasicUser, th.BasicChannel)
|
|
||||||
|
|
||||||
user1 := th.CreateUser()
|
|
||||||
user2 := th.CreateUser()
|
|
||||||
directChannel, err := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
referencedPost := &model.Post{
|
|
||||||
ChannelId: th.BasicChannel.Id,
|
|
||||||
Message: "hello world",
|
|
||||||
UserId: th.BasicUser.Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
th.Context.Session().UserId = th.BasicUser.Id
|
|
||||||
|
|
||||||
referencedPost, err = th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, false, false)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
Description string
|
|
||||||
Channel *model.Channel
|
|
||||||
Author string
|
|
||||||
Assert func(t assert.TestingT, object any, msgAndArgs ...any) bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Description: "removes metadata from post for members who cannot read channel",
|
|
||||||
Channel: directChannel,
|
|
||||||
Author: user1.Id,
|
|
||||||
Assert: assert.Nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Description: "does not remove metadata from post for members who can read channel",
|
|
||||||
Channel: th.BasicChannel,
|
|
||||||
Author: th.BasicUser.Id,
|
|
||||||
Assert: assert.NotNil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
t.Run(testCase.Description, func(t *testing.T) {
|
|
||||||
previewPost := &model.Post{
|
|
||||||
ChannelId: testCase.Channel.Id,
|
|
||||||
UserId: testCase.Author,
|
|
||||||
}
|
|
||||||
|
|
||||||
previewPost, err = th.App.CreatePost(th.Context, previewPost, testCase.Channel, false, false)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
previewPost.Message = permalink
|
|
||||||
previewPost, err = th.App.UpdatePost(th.Context, previewPost, false)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
testCase.Assert(t, previewPost.Metadata.Embeds[0].Data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchPostsForUser(t *testing.T) {
|
func TestSearchPostsForUser(t *testing.T) {
|
||||||
@@ -3043,26 +2974,64 @@ func TestGetPostIfAuthorized(t *testing.T) {
|
|||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
privateChannel := th.CreatePrivateChannel(th.Context, th.BasicTeam)
|
t.Run("Private channel", func(t *testing.T) {
|
||||||
post, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: privateChannel.Id, Message: "Hello"}, privateChannel, false, false)
|
privateChannel := th.CreatePrivateChannel(th.Context, th.BasicTeam)
|
||||||
require.Nil(t, err)
|
post, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: privateChannel.Id, Message: "Hello"}, privateChannel, false, false)
|
||||||
require.NotNil(t, post)
|
require.Nil(t, err)
|
||||||
|
require.NotNil(t, post)
|
||||||
|
|
||||||
session1, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
session1, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.NotNil(t, session1)
|
require.NotNil(t, session1)
|
||||||
|
|
||||||
session2, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
|
session2, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.NotNil(t, session2)
|
require.NotNil(t, session2)
|
||||||
|
|
||||||
// User is not authorized to get post
|
// User is not authorized to get post
|
||||||
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
|
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
|
|
||||||
// User is authorized to get post
|
// User is authorized to get post
|
||||||
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
|
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Public channel", func(t *testing.T) {
|
||||||
|
publicChannel := th.CreateChannel(th.Context, th.BasicTeam)
|
||||||
|
post, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: publicChannel.Id, Message: "Hello"}, publicChannel, false, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.NotNil(t, post)
|
||||||
|
|
||||||
|
session1, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.NotNil(t, session1)
|
||||||
|
|
||||||
|
session2, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.NotNil(t, session2)
|
||||||
|
|
||||||
|
// User is authorized to get post
|
||||||
|
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// User is authorized to get post
|
||||||
|
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(c *model.Config) {
|
||||||
|
b := true
|
||||||
|
c.ComplianceSettings.Enable = &b
|
||||||
|
})
|
||||||
|
|
||||||
|
// User is not authorized to get post
|
||||||
|
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// User is authorized to get post
|
||||||
|
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotRefollowOnOthersReply(t *testing.T) {
|
func TestShouldNotRefollowOnOthersReply(t *testing.T) {
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export function goToChannelByChannelName(match: Match, history: History): Action
|
|||||||
const isSystemAdmin = UserUtils.isSystemAdmin(user?.roles);
|
const isSystemAdmin = UserUtils.isSystemAdmin(user?.roles);
|
||||||
if (isSystemAdmin) {
|
if (isSystemAdmin) {
|
||||||
if (channel?.type === Constants.PRIVATE_CHANNEL) {
|
if (channel?.type === Constants.PRIVATE_CHANNEL) {
|
||||||
const joinPromptResult = await dispatch(joinPrivateChannelPrompt(teamObj, channel));
|
const joinPromptResult = await dispatch(joinPrivateChannelPrompt(teamObj, channel.display_name));
|
||||||
if ('data' in joinPromptResult && !joinPromptResult.data.join) {
|
if ('data' in joinPromptResult && !joinPromptResult.data.join) {
|
||||||
return {data: undefined};
|
return {data: undefined};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import type {Post} from '@mattermost/types/posts';
|
|||||||
import {getChannel, getChannelMember, selectChannel, joinChannel, getChannelStats} from 'mattermost-redux/actions/channels';
|
import {getChannel, getChannelMember, selectChannel, joinChannel, getChannelStats} from 'mattermost-redux/actions/channels';
|
||||||
import {getPostThread} from 'mattermost-redux/actions/posts';
|
import {getPostThread} from 'mattermost-redux/actions/posts';
|
||||||
import {getMissingProfilesByIds} from 'mattermost-redux/actions/users';
|
import {getMissingProfilesByIds} from 'mattermost-redux/actions/users';
|
||||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
|
import {getCurrentChannel, getChannel as getChannelFromRedux} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getCurrentTeam, getTeam} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeam, getTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
@@ -88,25 +89,48 @@ export function focusPost(postId: string, returnTo = '', currentUserId: string,
|
|||||||
if (privateChannelJoinPromptVisible) {
|
if (privateChannelJoinPromptVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {data} = await dispatch(getPostThread(postId));
|
|
||||||
|
|
||||||
if (!data) {
|
let postInfo;
|
||||||
|
try {
|
||||||
|
postInfo = await Client4.getPostInfo(postId);
|
||||||
|
} catch (e) {
|
||||||
getHistory().replace(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=${returnTo}`);
|
getHistory().replace(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=${returnTo}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.first_inaccessible_post_time) {
|
const state = getState();
|
||||||
|
const currentTeam = getCurrentTeam(state);
|
||||||
|
|
||||||
|
if (!postInfo.has_joined_channel) {
|
||||||
|
// Prompt system admin before joining the private channel
|
||||||
|
const user = getCurrentUser(state);
|
||||||
|
if (postInfo.channel_type === Constants.PRIVATE_CHANNEL && isSystemAdmin(user.roles)) {
|
||||||
|
privateChannelJoinPromptVisible = true;
|
||||||
|
const joinPromptResult = await dispatch(joinPrivateChannelPrompt(currentTeam, postInfo.channel_display_name));
|
||||||
|
privateChannelJoinPromptVisible = false;
|
||||||
|
if ('data' in joinPromptResult && !joinPromptResult.data.join) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await dispatch(joinChannel(currentUserId, '', postInfo.channel_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data: threadData} = await dispatch(getPostThread(postId));
|
||||||
|
|
||||||
|
if (!threadData) {
|
||||||
|
getHistory().replace(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=${returnTo}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadData.first_inaccessible_post_time) {
|
||||||
getHistory().replace(`/error?type=${ErrorPageTypes.CLOUD_ARCHIVED}&returnTo=${returnTo}`);
|
getHistory().replace(`/error?type=${ErrorPageTypes.CLOUD_ARCHIVED}&returnTo=${returnTo}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = getState();
|
|
||||||
const isCollapsed = isCollapsedThreadsEnabled(state);
|
const isCollapsed = isCollapsedThreadsEnabled(state);
|
||||||
|
|
||||||
const channelId = data.posts[data.order[0]].channel_id;
|
const channelId = threadData.posts[threadData.order[0]].channel_id;
|
||||||
let channel = state.entities.channels.channels[channelId];
|
let channel = getChannelFromRedux(state, channelId);
|
||||||
const currentTeam = getCurrentTeam(state);
|
|
||||||
const teamId = currentTeam.id;
|
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
const {data: channelData} = await dispatch(getChannel(channelId));
|
const {data: channelData} = await dispatch(getChannel(channelId));
|
||||||
@@ -119,6 +143,7 @@ export function focusPost(postId: string, returnTo = '', currentUserId: string,
|
|||||||
channel = channelData;
|
channel = channelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const teamId = channel.team_id || currentTeam.id;
|
||||||
let myMember = state.entities.channels.myMembers[channelId];
|
let myMember = state.entities.channels.myMembers[channelId];
|
||||||
|
|
||||||
if (!myMember) {
|
if (!myMember) {
|
||||||
@@ -134,17 +159,8 @@ export function focusPost(postId: string, returnTo = '', currentUserId: string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!myMember) {
|
if (!myMember) {
|
||||||
// Prompt system admin before joining the private channel
|
getHistory().replace(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=${returnTo}`);
|
||||||
const user = getCurrentUser(state);
|
return;
|
||||||
if (channel.type === Constants.PRIVATE_CHANNEL && isSystemAdmin(user.roles)) {
|
|
||||||
privateChannelJoinPromptVisible = true;
|
|
||||||
const joinPromptResult = await dispatch(joinPrivateChannelPrompt(currentTeam, channel));
|
|
||||||
privateChannelJoinPromptVisible = false;
|
|
||||||
if ('data' in joinPromptResult && !joinPromptResult.data.join) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await dispatch(joinChannel(currentUserId, '', channelId, channel.name));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +177,7 @@ export function focusPost(postId: string, returnTo = '', currentUserId: string,
|
|||||||
dispatch(loadNewGMIfNeeded(channel.id));
|
dispatch(loadNewGMIfNeeded(channel.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = data.posts[postId];
|
const post = threadData.posts[postId];
|
||||||
|
|
||||||
if (isCollapsed && isComment(post)) {
|
if (isCollapsed && isComment(post)) {
|
||||||
const {data} = await dispatch(focusReplyPost(post, channel, teamId, returnTo, option));
|
const {data} = await dispatch(focusReplyPost(post, channel, teamId, returnTo, option));
|
||||||
|
|||||||
@@ -174,24 +174,46 @@ describe('components/PermalinkView', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('focusPost', () => {
|
describe('focusPost', () => {
|
||||||
test('should redirect to error page for DM channel not a member of', async () => {
|
beforeEach(() => {
|
||||||
const testStore = await mockStore(initialState);
|
TestHelper.initBasic(Client4);
|
||||||
await testStore.dispatch(focusPost('dmpostid1', undefined, baseProps.currentUserId));
|
});
|
||||||
|
|
||||||
expect(getPostThread).toHaveBeenCalledWith('dmpostid1');
|
afterEach(() => {
|
||||||
|
TestHelper.tearDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
function nockInfoForPost(postId: string) {
|
||||||
|
nock(Client4.getPostRoute(postId)).
|
||||||
|
get('/info').
|
||||||
|
reply(200, {
|
||||||
|
has_joined_channel: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
test('should redirect to error page for DM channel not a member of', async () => {
|
||||||
|
const postId = 'dmpostid1';
|
||||||
|
TestHelper.initBasic(Client4);
|
||||||
|
nockInfoForPost(postId);
|
||||||
|
|
||||||
|
const testStore = await mockStore(initialState);
|
||||||
|
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
|
||||||
|
|
||||||
|
expect(getPostThread).toHaveBeenCalledWith(postId);
|
||||||
expect(testStore.getActions()).toEqual([
|
expect(testStore.getActions()).toEqual([
|
||||||
{type: 'MOCK_GET_POST_THREAD', data: {posts: {dmpostid1: {id: 'dmpostid1', message: 'some message', channel_id: 'dmchannelid'}}, order: ['dmpostid1']}},
|
{type: 'MOCK_GET_POST_THREAD', data: {posts: {dmpostid1: {id: postId, message: 'some message', channel_id: 'dmchannelid'}}, order: ['dmpostid1']}},
|
||||||
]);
|
]);
|
||||||
expect(getHistory().replace).toHaveBeenCalledWith(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=`);
|
expect(getHistory().replace).toHaveBeenCalledWith(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should redirect to error page for GM channel not a member of', async () => {
|
test('should redirect to error page for GM channel not a member of', async () => {
|
||||||
const testStore = await mockStore(initialState);
|
const postId = 'gmpostid1';
|
||||||
await testStore.dispatch(focusPost('gmpostid1', undefined, baseProps.currentUserId));
|
nockInfoForPost(postId);
|
||||||
|
|
||||||
expect(getPostThread).toHaveBeenCalledWith('gmpostid1');
|
const testStore = await mockStore(initialState);
|
||||||
|
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
|
||||||
|
|
||||||
|
expect(getPostThread).toHaveBeenCalledWith(postId);
|
||||||
expect(testStore.getActions()).toEqual([
|
expect(testStore.getActions()).toEqual([
|
||||||
{type: 'MOCK_GET_POST_THREAD', data: {posts: {gmpostid1: {id: 'gmpostid1', message: 'some message', channel_id: 'gmchannelid'}}, order: ['gmpostid1']}},
|
{type: 'MOCK_GET_POST_THREAD', data: {posts: {gmpostid1: {id: postId, message: 'some message', channel_id: 'gmchannelid'}}, order: ['gmpostid1']}},
|
||||||
]);
|
]);
|
||||||
expect(getHistory().replace).toHaveBeenCalledWith(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=`);
|
expect(getHistory().replace).toHaveBeenCalledWith(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=`);
|
||||||
});
|
});
|
||||||
@@ -199,7 +221,9 @@ describe('components/PermalinkView', () => {
|
|||||||
test('should redirect to DM link with postId for permalink', async () => {
|
test('should redirect to DM link with postId for permalink', async () => {
|
||||||
const dateNowOrig = Date.now;
|
const dateNowOrig = Date.now;
|
||||||
Date.now = () => new Date(0).getMilliseconds();
|
Date.now = () => new Date(0).getMilliseconds();
|
||||||
const nextTick = () => new Promise((res) => process.nextTick(res));
|
|
||||||
|
const postId = 'dmpostid1';
|
||||||
|
nockInfoForPost(postId);
|
||||||
|
|
||||||
TestHelper.initBasic(Client4);
|
TestHelper.initBasic(Client4);
|
||||||
nock(Client4.getUsersRoute()).
|
nock(Client4.getUsersRoute()).
|
||||||
@@ -220,13 +244,12 @@ describe('components/PermalinkView', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testStore = mockStore(modifiedState);
|
const testStore = mockStore(modifiedState);
|
||||||
testStore.dispatch(focusPost('dmpostid1', undefined, baseProps.currentUserId));
|
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
expect(getPostThread).toHaveBeenCalledWith('dmpostid1');
|
expect(getPostThread).toHaveBeenCalledWith(postId);
|
||||||
expect(testStore.getActions()).toEqual([
|
expect(testStore.getActions()).toEqual([
|
||||||
{type: 'MOCK_GET_POST_THREAD', data: {posts: {dmpostid1: {id: 'dmpostid1', message: 'some message', channel_id: 'dmchannelid'}}, order: ['dmpostid1']}},
|
{type: 'MOCK_GET_POST_THREAD', data: {posts: {dmpostid1: {id: postId, message: 'some message', channel_id: 'dmchannelid'}}, order: [postId]}},
|
||||||
{type: 'MOCK_GET_MISSING_PROFILES', userIds: ['dmchannel']},
|
{type: 'MOCK_GET_MISSING_PROFILES', userIds: ['dmchannel']},
|
||||||
{
|
{
|
||||||
type: 'RECEIVED_PREFERENCES',
|
type: 'RECEIVED_PREFERENCES',
|
||||||
@@ -236,17 +259,19 @@ describe('components/PermalinkView', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{type: 'MOCK_SELECT_CHANNEL', args: ['dmchannelid']},
|
{type: 'MOCK_SELECT_CHANNEL', args: ['dmchannelid']},
|
||||||
{type: 'RECEIVED_FOCUSED_POST', channelId: 'dmchannelid', data: 'dmpostid1'},
|
{type: 'RECEIVED_FOCUSED_POST', channelId: 'dmchannelid', data: postId},
|
||||||
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
|
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
|
||||||
{type: 'MOCK_GET_CHANNEL_STATS', args: ['dmchannelid']},
|
{type: 'MOCK_GET_CHANNEL_STATS', args: ['dmchannelid']},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(getHistory().replace).toHaveBeenCalledWith('/currentteam/messages/@otherUser/dmpostid1');
|
expect(getHistory().replace).toHaveBeenCalledWith('/currentteam/messages/@otherUser/dmpostid1');
|
||||||
Date.now = dateNowOrig;
|
Date.now = dateNowOrig;
|
||||||
TestHelper.tearDown();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should redirect to GM link with postId for permalink', async () => {
|
test('should redirect to GM link with postId for permalink', async () => {
|
||||||
|
const postId = 'gmpostid1';
|
||||||
|
nockInfoForPost(postId);
|
||||||
|
|
||||||
const modifiedState = {
|
const modifiedState = {
|
||||||
entities: {
|
entities: {
|
||||||
...initialState.entities,
|
...initialState.entities,
|
||||||
@@ -261,13 +286,13 @@ describe('components/PermalinkView', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testStore = await mockStore(modifiedState);
|
const testStore = await mockStore(modifiedState);
|
||||||
await testStore.dispatch(focusPost('gmpostid1', undefined, baseProps.currentUserId));
|
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
|
||||||
|
|
||||||
expect(getPostThread).toHaveBeenCalledWith('gmpostid1');
|
expect(getPostThread).toHaveBeenCalledWith(postId);
|
||||||
expect(testStore.getActions()).toEqual([
|
expect(testStore.getActions()).toEqual([
|
||||||
{type: 'MOCK_GET_POST_THREAD', data: {posts: {gmpostid1: {id: 'gmpostid1', message: 'some message', channel_id: 'gmchannelid'}}, order: ['gmpostid1']}},
|
{type: 'MOCK_GET_POST_THREAD', data: {posts: {gmpostid1: {id: postId, message: 'some message', channel_id: 'gmchannelid'}}, order: [postId]}},
|
||||||
{type: 'MOCK_SELECT_CHANNEL', args: ['gmchannelid']},
|
{type: 'MOCK_SELECT_CHANNEL', args: ['gmchannelid']},
|
||||||
{type: 'RECEIVED_FOCUSED_POST', channelId: 'gmchannelid', data: 'gmpostid1'},
|
{type: 'RECEIVED_FOCUSED_POST', channelId: 'gmchannelid', data: postId},
|
||||||
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
|
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
|
||||||
{type: 'MOCK_GET_CHANNEL_STATS', args: ['gmchannelid']},
|
{type: 'MOCK_GET_CHANNEL_STATS', args: ['gmchannelid']},
|
||||||
]);
|
]);
|
||||||
@@ -275,23 +300,26 @@ describe('components/PermalinkView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should redirect to channel link with postId for permalink', async () => {
|
test('should redirect to channel link with postId for permalink', async () => {
|
||||||
const testStore = await mockStore(initialState);
|
const postId = 'postid1';
|
||||||
await testStore.dispatch(focusPost('postid1', undefined, baseProps.currentUserId));
|
nockInfoForPost(postId);
|
||||||
|
|
||||||
expect(getPostThread).toHaveBeenCalledWith('postid1');
|
const testStore = await mockStore(initialState);
|
||||||
|
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
|
||||||
|
|
||||||
|
expect(getPostThread).toHaveBeenCalledWith(postId);
|
||||||
expect(testStore.getActions()).toEqual([
|
expect(testStore.getActions()).toEqual([
|
||||||
{
|
{
|
||||||
type: 'MOCK_GET_POST_THREAD',
|
type: 'MOCK_GET_POST_THREAD',
|
||||||
data: {
|
data: {
|
||||||
posts: {
|
posts: {
|
||||||
replypostid1: {id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
|
replypostid1: {id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: postId},
|
||||||
postid1: {id: 'postid1', message: 'some message', channel_id: 'channelid1'},
|
postid1: {id: postId, message: 'some message', channel_id: 'channelid1'},
|
||||||
},
|
},
|
||||||
order: ['postid1', 'replypostid1'],
|
order: [postId, 'replypostid1'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{type: 'MOCK_SELECT_CHANNEL', args: ['channelid1']},
|
{type: 'MOCK_SELECT_CHANNEL', args: ['channelid1']},
|
||||||
{type: 'RECEIVED_FOCUSED_POST', channelId: 'channelid1', data: 'postid1'},
|
{type: 'RECEIVED_FOCUSED_POST', channelId: 'channelid1', data: postId},
|
||||||
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
|
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
|
||||||
{type: 'MOCK_GET_CHANNEL_STATS', args: ['channelid1']},
|
{type: 'MOCK_GET_CHANNEL_STATS', args: ['channelid1']},
|
||||||
]);
|
]);
|
||||||
@@ -299,6 +327,9 @@ describe('components/PermalinkView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not redirect to channel link with postId for a reply permalink when collapsedThreads enabled and option is set true', async () => {
|
test('should not redirect to channel link with postId for a reply permalink when collapsedThreads enabled and option is set true', async () => {
|
||||||
|
const postId = 'replypostid1';
|
||||||
|
nockInfoForPost(postId);
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
entities: {
|
entities: {
|
||||||
...initialState.entities,
|
...initialState.entities,
|
||||||
@@ -313,34 +344,34 @@ describe('components/PermalinkView', () => {
|
|||||||
jest.spyOn<typeof Channels, keyof typeof Channels>(Channels, 'getCurrentChannel').mockReturnValue({id: 'channelid1', name: 'channel1', type: 'O', team_id: 'current_team_id'});
|
jest.spyOn<typeof Channels, keyof typeof Channels>(Channels, 'getCurrentChannel').mockReturnValue({id: 'channelid1', name: 'channel1', type: 'O', team_id: 'current_team_id'});
|
||||||
|
|
||||||
const testStore = await mockStore(newState);
|
const testStore = await mockStore(newState);
|
||||||
await testStore.dispatch(focusPost('replypostid1', '#', initialState.entities.users.currentUserId, {skipRedirectReplyPermalink: true}));
|
await testStore.dispatch(focusPost(postId, '#', initialState.entities.users.currentUserId, {skipRedirectReplyPermalink: true}));
|
||||||
|
|
||||||
expect(getPostThread).toHaveBeenCalledWith('replypostid1');
|
expect(getPostThread).toHaveBeenCalledWith(postId);
|
||||||
|
|
||||||
expect(testStore.getActions()).toEqual([
|
expect(testStore.getActions()).toEqual([
|
||||||
{
|
{
|
||||||
type: 'MOCK_GET_POST_THREAD',
|
type: 'MOCK_GET_POST_THREAD',
|
||||||
data: {
|
data: {
|
||||||
posts: {
|
posts: {
|
||||||
replypostid1: {id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
|
replypostid1: {id: postId, message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
|
||||||
postid1: {id: 'postid1', message: 'some message', channel_id: 'channelid1'},
|
postid1: {id: 'postid1', message: 'some message', channel_id: 'channelid1'},
|
||||||
|
|
||||||
},
|
},
|
||||||
order: ['postid1', 'replypostid1'],
|
order: ['postid1', postId],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'MOCK_GET_POST_THREAD',
|
type: 'MOCK_GET_POST_THREAD',
|
||||||
data: {
|
data: {
|
||||||
posts: {
|
posts: {
|
||||||
replypostid1: {id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
|
replypostid1: {id: postId, message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
|
||||||
postid1: {id: 'postid1', message: 'some message', channel_id: 'channelid1'},
|
postid1: {id: 'postid1', message: 'some message', channel_id: 'channelid1'},
|
||||||
|
|
||||||
},
|
},
|
||||||
order: ['postid1', 'replypostid1'],
|
order: ['postid1', postId],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{type: 'MOCK_SELECT_POST_AND_HIGHLIGHT', args: [{id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: 'postid1'}]},
|
{type: 'MOCK_SELECT_POST_AND_HIGHLIGHT', args: [{id: postId, message: 'some message', channel_id: 'channelid1', root_id: 'postid1'}]},
|
||||||
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
|
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
|
||||||
{type: 'MOCK_GET_CHANNEL_STATS', args: ['channelid1']},
|
{type: 'MOCK_GET_CHANNEL_STATS', args: ['channelid1']},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -206,6 +206,8 @@ function TeamController(props: Props) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const teamLoaded = team?.name.toLowerCase() === teamNameParam?.toLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
@@ -229,7 +231,7 @@ function TeamController(props: Props) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<ChannelController shouldRenderCenterChannel={initialChannelsLoaded}/>
|
<ChannelController shouldRenderCenterChannel={initialChannelsLoaded && teamLoaded}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,14 +71,14 @@ type JoinPrivateChannelPromptResult = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function joinPrivateChannelPrompt(team: Team, channel: Channel, handleOnCancel = true): ActionFunc {
|
export function joinPrivateChannelPrompt(team: Team, channelDisplayName: string, handleOnCancel = true): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const result: JoinPrivateChannelPromptResult = await new Promise((resolve) => {
|
const result: JoinPrivateChannelPromptResult = await new Promise((resolve) => {
|
||||||
const modalData = {
|
const modalData = {
|
||||||
modalId: ModalIdentifiers.JOIN_CHANNEL_PROMPT,
|
modalId: ModalIdentifiers.JOIN_CHANNEL_PROMPT,
|
||||||
dialogType: JoinPrivateChannelModal,
|
dialogType: JoinPrivateChannelModal,
|
||||||
dialogProps: {
|
dialogProps: {
|
||||||
channelName: channel.display_name,
|
channelName: channelDisplayName,
|
||||||
onJoin: () => {
|
onJoin: () => {
|
||||||
LocalStorageStore.setTeamIdJoinedOnLoad(null);
|
LocalStorageStore.setTeamIdJoinedOnLoad(null);
|
||||||
resolve({
|
resolve({
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ import type {
|
|||||||
MarketplaceApp,
|
MarketplaceApp,
|
||||||
MarketplacePlugin,
|
MarketplacePlugin,
|
||||||
} from '@mattermost/types/marketplace';
|
} from '@mattermost/types/marketplace';
|
||||||
import {Post, PostList, PostSearchResults, OpenGraphMetadata, PostsUsageResponse, TeamsUsageResponse, PaginatedPostList, FilesUsageResponse, PostAcknowledgement, PostAnalytics} from '@mattermost/types/posts';
|
import {Post, PostList, PostSearchResults, PostsUsageResponse, TeamsUsageResponse, PaginatedPostList, FilesUsageResponse, PostAcknowledgement, PostAnalytics, PostInfo} from '@mattermost/types/posts';
|
||||||
import {Draft} from '@mattermost/types/drafts';
|
import {Draft} from '@mattermost/types/drafts';
|
||||||
import {Reaction} from '@mattermost/types/reactions';
|
import {Reaction} from '@mattermost/types/reactions';
|
||||||
import {Role} from '@mattermost/types/roles';
|
import {Role} from '@mattermost/types/roles';
|
||||||
@@ -2160,6 +2160,13 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getPostInfo = (postId: string) => {
|
||||||
|
return this.doFetch<PostInfo>(
|
||||||
|
`${this.getPostRoute(postId)}/info`,
|
||||||
|
{method: 'get'},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
getPostsByIds = (postIds: string[]) => {
|
getPostsByIds = (postIds: string[]) => {
|
||||||
return this.doFetch<Post[]>(
|
return this.doFetch<Post[]>(
|
||||||
`${this.getPostsRoute()}/ids`,
|
`${this.getPostsRoute()}/ids`,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {Channel, ChannelType} from './channels';
|
|||||||
import {CustomEmoji} from './emojis';
|
import {CustomEmoji} from './emojis';
|
||||||
import {FileInfo} from './files';
|
import {FileInfo} from './files';
|
||||||
import {Reaction} from './reactions';
|
import {Reaction} from './reactions';
|
||||||
|
import { TeamType } from './teams';
|
||||||
import {UserProfile} from './users';
|
import {UserProfile} from './users';
|
||||||
import {
|
import {
|
||||||
RelationOneToOne,
|
RelationOneToOne,
|
||||||
@@ -208,4 +209,15 @@ export type ActivityEntry = {
|
|||||||
actorId: string[];
|
actorId: string[];
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
usernames: string[];
|
usernames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PostInfo = {
|
||||||
|
channel_id: string;
|
||||||
|
channel_type: ChannelType;
|
||||||
|
channel_display_name: string;
|
||||||
|
has_joined_channel: boolean;
|
||||||
|
team_id: string;
|
||||||
|
team_type: TeamType;
|
||||||
|
team_display_name: string;
|
||||||
|
has_joined_team: boolean;
|
||||||
}
|
}
|
||||||
Ссылка в новой задаче
Block a user