Files
team/server/channels/app/channel_join_request_test.go
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

380 lines
14 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hanzoteam/server/server/public/model"
)
// withDiscoverableChannelsFlag toggles the FeatureFlag for the duration of a
// test and restores it on cleanup. Feature flags are read-only by default in
// the test config store; flipping SetReadOnlyFF lets the UpdateConfig call
// land. We deliberately do NOT restore SetReadOnlyFF(true) afterward — the
// underlying store is per-test and disposed on cleanup.
func withDiscoverableChannelsFlag(t *testing.T, th *TestHelper, on bool) {
t.Helper()
th.ConfigStore.SetReadOnlyFF(false)
previous := th.App.Config().FeatureFlags.DiscoverableChannels
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.DiscoverableChannels = on })
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.DiscoverableChannels = previous })
})
}
// markDiscoverable flips the channel's discoverable flag in the store via
// PatchChannel so the model invariants run alongside the test scenario.
func markDiscoverable(t *testing.T, th *TestHelper, channel *model.Channel) *model.Channel {
t.Helper()
on := true
patched, err := th.App.PatchChannel(th.Context, channel, &model.ChannelPatch{Discoverable: &on}, th.BasicUser.Id)
require.Nil(t, err)
require.True(t, patched.Discoverable)
return patched
}
func TestRequestJoinChannel_RejectsNonDiscoverable(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := th.CreatePrivateChannel(t, th.BasicTeam)
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
joined, req, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channel.Id, "please")
require.NotNil(t, appErr)
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
assert.Equal(t, "api.channel.discoverable_join_request.not_discoverable.app_error", appErr.Id)
assert.False(t, joined)
assert.Nil(t, req)
}
func TestRequestJoinChannel_RejectsExistingMember(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := th.CreatePrivateChannel(t, th.BasicTeam)
channel = markDiscoverable(t, th, channel)
// BasicUser is the channel creator → already a member.
_, _, appErr := th.App.RequestJoinChannel(th.Context, th.BasicUser.Id, channel.Id, "")
require.NotNil(t, appErr)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
assert.Equal(t, "api.channel.discoverable_join_request.already_member.app_error", appErr.Id)
}
func TestRequestJoinChannel_PendingHappyPath(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := th.CreatePrivateChannel(t, th.BasicTeam)
channel = markDiscoverable(t, th, channel)
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
joined, req, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channel.Id, "let me in")
require.Nil(t, appErr)
assert.False(t, joined, "should not auto-join when no policy is enforced")
require.NotNil(t, req)
assert.Equal(t, model.ChannelJoinRequestStatusPending, req.Status)
assert.Equal(t, channel.Id, req.ChannelId)
assert.Equal(t, other.Id, req.UserId)
assert.Equal(t, "let me in", req.Message)
// Submitting again returns the existing pending row (idempotent on
// partial-unique conflict).
joined, req2, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channel.Id, "again")
require.Nil(t, appErr)
assert.False(t, joined)
require.NotNil(t, req2)
assert.Equal(t, req.Id, req2.Id)
}
func TestRequestJoinChannel_RejectsGuest(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := th.CreatePrivateChannel(t, th.BasicTeam)
channel = markDiscoverable(t, th, channel)
guest := th.CreateGuest(t)
th.LinkUserToTeam(t, guest, th.BasicTeam)
_, _, appErr := th.App.RequestJoinChannel(th.Context, guest.Id, channel.Id, "")
require.NotNil(t, appErr)
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
assert.Equal(t, "api.channel.discoverable_join_request.guest.app_error", appErr.Id)
}
func TestUpdateChannelJoinRequest_ApproveAddsMember(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := th.CreatePrivateChannel(t, th.BasicTeam)
channel = markDiscoverable(t, th, channel)
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
_, req, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channel.Id, "")
require.Nil(t, appErr)
require.NotNil(t, req)
patch := &model.ChannelJoinRequestPatch{Status: model.ChannelJoinRequestStatusApproved}
updated, appErr := th.App.UpdateChannelJoinRequest(th.Context, req.Id, channel.Id, patch, th.BasicUser.Id)
require.Nil(t, appErr)
assert.Equal(t, model.ChannelJoinRequestStatusApproved, updated.Status)
assert.Equal(t, th.BasicUser.Id, updated.ReviewedBy)
assert.NotZero(t, updated.ReviewedAt)
assert.Empty(t, updated.Message, "message should be redacted from the response after review")
member, mErr := th.App.GetChannelMember(th.Context, channel.Id, other.Id)
require.Nil(t, mErr)
require.NotNil(t, member)
assert.Equal(t, other.Id, member.UserId)
}
func TestUpdateChannelJoinRequest_DenyKeepsReason(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := th.CreatePrivateChannel(t, th.BasicTeam)
channel = markDiscoverable(t, th, channel)
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
_, req, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channel.Id, "please")
require.Nil(t, appErr)
require.NotNil(t, req)
reason := "team-internal channel"
patch := &model.ChannelJoinRequestPatch{
Status: model.ChannelJoinRequestStatusDenied,
DenialReason: &reason,
}
updated, appErr := th.App.UpdateChannelJoinRequest(th.Context, req.Id, channel.Id, patch, th.BasicUser.Id)
require.Nil(t, appErr)
assert.Equal(t, model.ChannelJoinRequestStatusDenied, updated.Status)
assert.Equal(t, reason, updated.DenialReason)
// Member must NOT have been added.
_, mErr := th.App.GetChannelMember(th.Context, channel.Id, other.Id)
require.NotNil(t, mErr)
assert.Equal(t, MissingChannelMemberError, mErr.Id)
}
func TestUpdateChannelJoinRequest_RejectsCrossChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channelA := markDiscoverable(t, th, th.CreatePrivateChannel(t, th.BasicTeam))
channelB := markDiscoverable(t, th, th.CreatePrivateChannel(t, th.BasicTeam))
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
_, req, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channelA.Id, "")
require.Nil(t, appErr)
require.NotNil(t, req)
patch := &model.ChannelJoinRequestPatch{Status: model.ChannelJoinRequestStatusApproved}
_, appErr = th.App.UpdateChannelJoinRequest(th.Context, req.Id, channelB.Id, patch, th.BasicUser.Id)
require.NotNil(t, appErr)
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
}
func TestWithdrawChannelJoinRequest_OwnerOnly(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := markDiscoverable(t, th, th.CreatePrivateChannel(t, th.BasicTeam))
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
_, req, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channel.Id, "")
require.Nil(t, appErr)
require.NotNil(t, req)
stranger := th.CreateUser(t)
_, appErr = th.App.WithdrawChannelJoinRequest(th.Context, req.Id, stranger.Id)
require.NotNil(t, appErr)
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
updated, appErr := th.App.WithdrawChannelJoinRequest(th.Context, req.Id, other.Id)
require.Nil(t, appErr)
assert.Equal(t, model.ChannelJoinRequestStatusWithdrawn, updated.Status)
// A second withdrawal is rejected with 409.
_, appErr = th.App.WithdrawChannelJoinRequest(th.Context, req.Id, other.Id)
require.NotNil(t, appErr)
assert.Equal(t, http.StatusConflict, appErr.StatusCode)
}
func TestGetMyChannelJoinRequests(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channelA := markDiscoverable(t, th, th.CreatePrivateChannel(t, th.BasicTeam))
channelB := markDiscoverable(t, th, th.CreatePrivateChannel(t, th.BasicTeam))
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
_, _, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channelA.Id, "")
require.Nil(t, appErr)
_, _, appErr = th.App.RequestJoinChannel(th.Context, other.Id, channelB.Id, "")
require.Nil(t, appErr)
list, appErr := th.App.GetMyChannelJoinRequests(th.Context, other.Id, model.GetChannelJoinRequestsOpts{})
require.Nil(t, appErr)
require.NotNil(t, list)
assert.EqualValues(t, 2, list.TotalCount)
assert.Len(t, list.Requests, 2)
}
func TestCountPendingChannelJoinRequests(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := markDiscoverable(t, th, th.CreatePrivateChannel(t, th.BasicTeam))
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
_, _, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channel.Id, "")
require.Nil(t, appErr)
count, appErr := th.App.CountPendingChannelJoinRequests(th.Context, channel.Id)
require.Nil(t, appErr)
assert.EqualValues(t, 1, count)
}
func TestUpdateChannelPrivacy_CancelsPendingRequestsOnConvertToPublic(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := markDiscoverable(t, th, th.CreatePrivateChannel(t, th.BasicTeam))
other := th.CreateUser(t)
th.LinkUserToTeam(t, other, th.BasicTeam)
_, req, appErr := th.App.RequestJoinChannel(th.Context, other.Id, channel.Id, "")
require.Nil(t, appErr)
require.NotNil(t, req)
channel.Type = model.ChannelTypeOpen
converted, appErr := th.App.UpdateChannelPrivacy(th.Context, channel, th.BasicUser)
require.Nil(t, appErr)
// Discoverable must be reset on convert-to-public — the model invariant
// (Channel.IsValid) rejects (type=O, discoverable=true), so leaving it
// true would also break the next channel save.
assert.False(t, converted.Discoverable, "Discoverable must be reset to false after convert-to-public")
persisted, getErr := th.App.GetChannel(th.Context, channel.Id)
require.Nil(t, getErr)
assert.False(t, persisted.Discoverable, "Discoverable must be persisted as false after convert-to-public")
// The cancellation side-effect is dispatched on a goroutine; poll for
// the withdrawn state instead of sleeping.
require.Eventually(t, func() bool {
row, err := th.App.Srv().Store().ChannelJoinRequest().Get(req.Id)
if err != nil {
return false
}
return row.Status == model.ChannelJoinRequestStatusWithdrawn
}, 2*time.Second, 50*time.Millisecond)
}
func TestIsDiscoverableSelfAddBlocked(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := markDiscoverable(t, th, th.CreatePrivateChannel(t, th.BasicTeam))
other := th.CreateUser(t)
assert.True(t, th.App.IsDiscoverableSelfAddBlocked(th.Context, channel, other.Id, other.Id), "self-add to discoverable + no-policy private must be blocked")
assert.False(t, th.App.IsDiscoverableSelfAddBlocked(th.Context, channel, th.BasicUser.Id, other.Id), "admin invite must not be blocked")
// Toggle off the flag → guard is inert.
withDiscoverableChannelsFlag(t, th, false)
assert.False(t, th.App.IsDiscoverableSelfAddBlocked(th.Context, channel, other.Id, other.Id))
}
func TestFilterDiscoverableChannelsByPolicy_FlagOff(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// Flag off → filter is a no-op even when channels look discoverable.
channel := markDiscoverableInMemory(t, th.CreatePrivateChannel(t, th.BasicTeam))
channel.PolicyEnforced = true
out, appErr := th.App.FilterDiscoverableChannelsByPolicy(th.Context, []*model.Channel{channel}, th.BasicUser2.Id)
require.Nil(t, appErr)
require.Len(t, out, 1)
}
func TestFilterDiscoverableChannelsByPolicy_NoPolicyPasses(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := markDiscoverableInMemory(t, th.CreatePrivateChannel(t, th.BasicTeam))
out, appErr := th.App.FilterDiscoverableChannelsByPolicy(th.Context, []*model.Channel{channel}, th.BasicUser2.Id)
require.Nil(t, appErr)
require.Len(t, out, 1, "no-policy discoverable channels are visible without ABAC evaluation")
}
func TestFilterDiscoverableChannelsByPolicy_PolicyEnforcedFailSecure(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
// PolicyEnforced + Discoverable + no AccessControl service wired ⇒ hidden.
channel := markDiscoverableInMemory(t, th.CreatePrivateChannel(t, th.BasicTeam))
channel.PolicyEnforced = true
require.Nil(t, th.App.Srv().Channels().AccessControl, "test fixture must not have ABAC wired")
out, appErr := th.App.FilterDiscoverableChannelsByPolicy(th.Context, []*model.Channel{channel}, th.BasicUser2.Id)
require.Nil(t, appErr)
assert.Len(t, out, 0, "fail-secure must hide policy-enforced channels when ABAC is unavailable")
}
func TestFilterDiscoverableChannelsByPolicy_GuestHidden(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
withDiscoverableChannelsFlag(t, th, true)
channel := markDiscoverableInMemory(t, th.CreatePrivateChannel(t, th.BasicTeam))
channel.PolicyEnforced = true
guest := th.CreateGuest(t)
out, appErr := th.App.FilterDiscoverableChannelsByPolicy(th.Context, []*model.Channel{channel}, guest.Id)
require.Nil(t, appErr)
assert.Empty(t, out, "guests must never see discoverable + policy-enforced channels")
}
// markDiscoverableInMemory is a no-DB helper for visibility filter tests that
// don't care about persistence — they only exercise the in-memory list filter.
func markDiscoverableInMemory(t *testing.T, channel *model.Channel) *model.Channel {
t.Helper()
channel.Discoverable = true
return channel
}