mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
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.
380 lines
14 KiB
Go
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
|
|
}
|