snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
5
server/cmd/mmctl/README.md
Normal file
5
server/cmd/mmctl/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# mmctl
|
||||
|
||||
`mmctl` is a remote CLI tool that lets you manage your Mattermost instance from the terminal.
|
||||
|
||||
The project's documentation can be accessed within the [the Mattermost documentation](https://docs.mattermost.com/manage/mmctl-command-line-tool.html).
|
||||
187
server/cmd/mmctl/client/client.go
Normal file
187
server/cmd/mmctl/client/client.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
CreateChannel(ctx context.Context, channel *model.Channel) (*model.Channel, *model.Response, error)
|
||||
CreateDirectChannel(ctx context.Context, userID1, userID2 string) (*model.Channel, *model.Response, error)
|
||||
RemoveUserFromChannel(ctx context.Context, channelID, userID string) (*model.Response, error)
|
||||
GetChannelMembers(ctx context.Context, channelID string, page, perPage int, etag string) (model.ChannelMembers, *model.Response, error)
|
||||
AddChannelMember(ctx context.Context, channelID, userID string) (*model.ChannelMember, *model.Response, error)
|
||||
DeleteChannel(ctx context.Context, channelID string) (*model.Response, error)
|
||||
PermanentDeleteChannel(ctx context.Context, channelID string) (*model.Response, error)
|
||||
MoveChannel(ctx context.Context, channelID, teamID string, force bool) (*model.Channel, *model.Response, error)
|
||||
GetPublicChannelsForTeam(ctx context.Context, teamID string, page int, perPage int, etag string) ([]*model.Channel, *model.Response, error)
|
||||
GetDeletedChannelsForTeam(ctx context.Context, teamID string, page int, perPage int, etag string) ([]*model.Channel, *model.Response, error)
|
||||
GetPrivateChannelsForTeam(ctx context.Context, teamID string, page int, perPage int, etag string) ([]*model.Channel, *model.Response, error)
|
||||
GetChannelsForTeamForUser(ctx context.Context, teamID, userID string, includeDeleted bool, etag string) ([]*model.Channel, *model.Response, error)
|
||||
RestoreChannel(ctx context.Context, channelID string) (*model.Channel, *model.Response, error)
|
||||
PatchChannel(ctx context.Context, channelID string, patch *model.ChannelPatch) (*model.Channel, *model.Response, error)
|
||||
GetChannelByName(ctx context.Context, channelName, teamID string, etag string) (*model.Channel, *model.Response, error)
|
||||
GetChannelByNameIncludeDeleted(ctx context.Context, channelName, teamID string, etag string) (*model.Channel, *model.Response, error)
|
||||
GetChannel(ctx context.Context, channelID string) (*model.Channel, *model.Response, error)
|
||||
GetTeam(ctx context.Context, teamID, etag string) (*model.Team, *model.Response, error)
|
||||
GetTeamByName(ctx context.Context, name, etag string) (*model.Team, *model.Response, error)
|
||||
GetAllTeams(ctx context.Context, etag string, page int, perPage int) ([]*model.Team, *model.Response, error)
|
||||
CreateTeam(ctx context.Context, team *model.Team) (*model.Team, *model.Response, error)
|
||||
PatchTeam(ctx context.Context, teamID string, patch *model.TeamPatch) (*model.Team, *model.Response, error)
|
||||
AddTeamMember(ctx context.Context, teamID, userID string) (*model.TeamMember, *model.Response, error)
|
||||
GetTeamMembersByIds(ctx context.Context, teamID string, userIDs []string) ([]*model.TeamMember, *model.Response, error)
|
||||
RemoveTeamMember(ctx context.Context, teamID, userID string) (*model.Response, error)
|
||||
SoftDeleteTeam(ctx context.Context, teamID string) (*model.Response, error)
|
||||
PermanentDeleteTeam(ctx context.Context, teamID string) (*model.Response, error)
|
||||
RestoreTeam(ctx context.Context, teamID string) (*model.Team, *model.Response, error)
|
||||
UpdateTeamPrivacy(ctx context.Context, teamID string, privacy string) (*model.Team, *model.Response, error)
|
||||
SearchTeams(ctx context.Context, search *model.TeamSearch) ([]*model.Team, *model.Response, error)
|
||||
GetPost(ctx context.Context, postID string, etag string) (*model.Post, *model.Response, error)
|
||||
CreatePost(ctx context.Context, post *model.Post) (*model.Post, *model.Response, error)
|
||||
RevealPost(ctx context.Context, postID string) (*model.Post, *model.Response, error)
|
||||
GetPostsForChannel(ctx context.Context, channelID string, page, perPage int, etag string, collapsedThreads bool, includeDeleted bool) (*model.PostList, *model.Response, error)
|
||||
GetPostsSince(ctx context.Context, channelID string, since int64, collapsedThreads bool) (*model.PostList, *model.Response, error)
|
||||
DoAPIPost(ctx context.Context, url string, data string) (*http.Response, error)
|
||||
GetLdapGroups(ctx context.Context) ([]*model.Group, *model.Response, error)
|
||||
GetGroupsByChannel(ctx context.Context, channelID string, groupOpts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.Response, error)
|
||||
GetGroupsByTeam(ctx context.Context, teamID string, groupOpts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.Response, error)
|
||||
RestoreGroup(ctx context.Context, groupID string, etag string) (*model.Group, *model.Response, error)
|
||||
GetOldClientLicense(ctx context.Context, etag string) (map[string]string, *model.Response, error)
|
||||
UploadLicenseFile(ctx context.Context, data []byte) (*model.Response, error)
|
||||
RemoveLicenseFile(ctx context.Context) (*model.Response, error)
|
||||
GetLogs(ctx context.Context, page, perPage int) ([]string, *model.Response, error)
|
||||
GetRoleByName(ctx context.Context, name string) (*model.Role, *model.Response, error)
|
||||
GetAllRoles(ctx context.Context) ([]*model.Role, *model.Response, error)
|
||||
PatchRole(ctx context.Context, roleID string, patch *model.RolePatch) (*model.Role, *model.Response, error)
|
||||
UploadPlugin(ctx context.Context, file io.Reader) (*model.Manifest, *model.Response, error)
|
||||
UploadPluginForced(ctx context.Context, file io.Reader) (*model.Manifest, *model.Response, error)
|
||||
RemovePlugin(ctx context.Context, id string) (*model.Response, error)
|
||||
EnablePlugin(ctx context.Context, id string) (*model.Response, error)
|
||||
DisablePlugin(ctx context.Context, id string) (*model.Response, error)
|
||||
GetPlugins(ctx context.Context) (*model.PluginsResponse, *model.Response, error)
|
||||
GetMe(ctx context.Context, etag string) (*model.User, *model.Response, error)
|
||||
GetUser(ctx context.Context, userID, etag string) (*model.User, *model.Response, error)
|
||||
GetUserByUsername(ctx context.Context, userName, etag string) (*model.User, *model.Response, error)
|
||||
GetUserByEmail(ctx context.Context, email, etag string) (*model.User, *model.Response, error)
|
||||
GetUsersByIds(ctx context.Context, userIDs []string) ([]*model.User, *model.Response, error)
|
||||
GetUsersWithCustomQueryParameters(ctx context.Context, page, perPage int, queryParameters string, etag string) ([]*model.User, *model.Response, error)
|
||||
GetUsersInTeam(ctx context.Context, teamID string, page, perPage int, etag string) ([]*model.User, *model.Response, error)
|
||||
GetUsersInChannel(ctx context.Context, channelID string, page, perPage int, etag string) ([]*model.User, *model.Response, error)
|
||||
PermanentDeleteUser(ctx context.Context, userID string) (*model.Response, error)
|
||||
PermanentDeleteAllUsers(ctx context.Context) (*model.Response, error)
|
||||
CreateUser(ctx context.Context, user *model.User) (*model.User, *model.Response, error)
|
||||
VerifyUserEmailWithoutToken(ctx context.Context, userID string) (*model.User, *model.Response, error)
|
||||
UpdateUserRoles(ctx context.Context, userID, roles string) (*model.Response, error)
|
||||
InviteUsersToTeam(ctx context.Context, teamID string, userEmails []string) (*model.Response, error)
|
||||
SendPasswordResetEmail(ctx context.Context, email string) (*model.Response, error)
|
||||
UpdateUser(ctx context.Context, user *model.User) (*model.User, *model.Response, error)
|
||||
UpdateUserAuth(ctx context.Context, userId string, userAuth *model.UserAuth) (*model.UserAuth, *model.Response, error)
|
||||
UpdateUserMfa(ctx context.Context, userID, code string, activate bool) (*model.Response, error)
|
||||
UpdateUserPassword(ctx context.Context, userID, currentPassword, newPassword string) (*model.Response, error)
|
||||
UpdateUserHashedPassword(ctx context.Context, userID, newHashedPassword string) (*model.Response, error)
|
||||
CreateUserAccessToken(ctx context.Context, userID, description string, expiresAt int64) (*model.UserAccessToken, *model.Response, error)
|
||||
RevokeUserAccessToken(ctx context.Context, tokenID string) (*model.Response, error)
|
||||
RotateUserAccessToken(ctx context.Context, tokenID string, expiresAt int64) (*model.UserAccessToken, *model.Response, error)
|
||||
GetUserAccessTokensForUser(ctx context.Context, userID string, page, perPage int) ([]*model.UserAccessToken, *model.Response, error)
|
||||
ConvertUserToBot(ctx context.Context, userID string) (*model.Bot, *model.Response, error)
|
||||
ConvertBotToUser(ctx context.Context, userID string, userPatch *model.UserPatch, setSystemAdmin bool) (*model.User, *model.Response, error)
|
||||
PromoteGuestToUser(ctx context.Context, userID string) (*model.Response, error)
|
||||
DemoteUserToGuest(ctx context.Context, guestID string) (*model.Response, error)
|
||||
GetUserStatus(ctx context.Context, userID, etag string) (*model.Status, *model.Response, error)
|
||||
UpdateUserStatus(ctx context.Context, userID string, userStatus *model.Status) (*model.Status, *model.Response, error)
|
||||
CreateCommand(ctx context.Context, cmd *model.Command) (*model.Command, *model.Response, error)
|
||||
ListCommands(ctx context.Context, teamID string, customOnly bool) ([]*model.Command, *model.Response, error)
|
||||
GetCommandById(ctx context.Context, cmdID string) (*model.Command, *model.Response, error)
|
||||
UpdateCommand(ctx context.Context, cmd *model.Command) (*model.Command, *model.Response, error)
|
||||
MoveCommand(ctx context.Context, teamID string, commandID string) (*model.Response, error)
|
||||
DeleteCommand(ctx context.Context, commandID string) (*model.Response, error)
|
||||
GetConfig(ctx context.Context) (*model.Config, *model.Response, error)
|
||||
GetConfigWithOptions(ctx context.Context, options model.GetConfigOptions) (map[string]any, *model.Response, error)
|
||||
GetClientConfig(ctx context.Context, etag string) (map[string]string, *model.Response, error)
|
||||
UpdateConfig(context.Context, *model.Config) (*model.Config, *model.Response, error)
|
||||
PatchConfig(context.Context, *model.Config) (*model.Config, *model.Response, error)
|
||||
ReloadConfig(ctx context.Context) (*model.Response, error)
|
||||
MigrateConfig(ctx context.Context, from, to string) (*model.Response, error)
|
||||
SyncLdap(ctx context.Context) (*model.Response, error)
|
||||
MigrateIdLdap(ctx context.Context, toAttribute string) (*model.Response, error)
|
||||
GetUsers(ctx context.Context, page, perPage int, etag string) ([]*model.User, *model.Response, error)
|
||||
UpdateUserActive(ctx context.Context, userID string, activate bool) (*model.Response, error)
|
||||
UpdateTeam(ctx context.Context, team *model.Team) (*model.Team, *model.Response, error)
|
||||
UpdateChannelPrivacy(ctx context.Context, channelID string, privacy model.ChannelType) (*model.Channel, *model.Response, error)
|
||||
CreateBot(ctx context.Context, bot *model.Bot) (*model.Bot, *model.Response, error)
|
||||
PatchBot(ctx context.Context, userID string, patch *model.BotPatch) (*model.Bot, *model.Response, error)
|
||||
GetBots(ctx context.Context, page, perPage int, etag string) ([]*model.Bot, *model.Response, error)
|
||||
GetBotsIncludeDeleted(ctx context.Context, page, perPage int, etag string) ([]*model.Bot, *model.Response, error)
|
||||
GetBotsOrphaned(ctx context.Context, page, perPage int, etag string) ([]*model.Bot, *model.Response, error)
|
||||
DisableBot(ctx context.Context, botUserID string) (*model.Bot, *model.Response, error)
|
||||
EnableBot(ctx context.Context, botUserID string) (*model.Bot, *model.Response, error)
|
||||
AssignBot(ctx context.Context, botUserID, newOwnerID string) (*model.Bot, *model.Response, error)
|
||||
SetServerBusy(ctx context.Context, secs int) (*model.Response, error)
|
||||
ClearServerBusy(ctx context.Context) (*model.Response, error)
|
||||
GetServerBusy(ctx context.Context) (*model.ServerBusyState, *model.Response, error)
|
||||
CheckIntegrity(ctx context.Context) ([]model.IntegrityCheckResult, *model.Response, error)
|
||||
InstallPluginFromURL(context.Context, string, bool) (*model.Manifest, *model.Response, error)
|
||||
InstallMarketplacePlugin(context.Context, *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.Response, error)
|
||||
GetMarketplacePlugins(context.Context, *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.Response, error)
|
||||
MigrateAuthToLdap(ctx context.Context, fromAuthService string, matchField string, force bool) (*model.Response, error)
|
||||
MigrateAuthToSaml(ctx context.Context, fromAuthService string, usersMap map[string]string, auto bool) (*model.Response, error)
|
||||
GetPing(ctx context.Context) (string, *model.Response, error)
|
||||
GetPingWithFullServerStatus(ctx context.Context) (map[string]any, *model.Response, error)
|
||||
GetPingWithOptions(ctx context.Context, options model.SystemPingOptions) (map[string]any, *model.Response, error)
|
||||
CreateUpload(ctx context.Context, us *model.UploadSession) (*model.UploadSession, *model.Response, error)
|
||||
GetUpload(ctx context.Context, uploadID string) (*model.UploadSession, *model.Response, error)
|
||||
GetUploadsForUser(ctx context.Context, userID string) ([]*model.UploadSession, *model.Response, error)
|
||||
UploadData(ctx context.Context, uploadID string, data io.Reader) (*model.FileInfo, *model.Response, error)
|
||||
ListImports(ctx context.Context) ([]string, *model.Response, error)
|
||||
DeleteImport(ctx context.Context, name string) (*model.Response, error)
|
||||
GetJob(ctx context.Context, id string) (*model.Job, *model.Response, error)
|
||||
GetJobs(ctx context.Context, jobType string, status string, page int, perPage int) ([]*model.Job, *model.Response, error)
|
||||
GetJobsByType(ctx context.Context, jobType string, page int, perPage int) ([]*model.Job, *model.Response, error)
|
||||
CreateJob(ctx context.Context, job *model.Job) (*model.Job, *model.Response, error)
|
||||
CancelJob(ctx context.Context, jobID string) (*model.Response, error)
|
||||
UpdateJobStatus(ctx context.Context, jobId string, status string, force bool) (*model.Response, error)
|
||||
CreateIncomingWebhook(ctx context.Context, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.Response, error)
|
||||
UpdateIncomingWebhook(ctx context.Context, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.Response, error)
|
||||
GetIncomingWebhooks(ctx context.Context, page int, perPage int, etag string) ([]*model.IncomingWebhook, *model.Response, error)
|
||||
GetIncomingWebhooksForTeam(ctx context.Context, teamID string, page int, perPage int, etag string) ([]*model.IncomingWebhook, *model.Response, error)
|
||||
GetIncomingWebhook(ctx context.Context, hookID string, etag string) (*model.IncomingWebhook, *model.Response, error)
|
||||
DeleteIncomingWebhook(ctx context.Context, hookID string) (*model.Response, error)
|
||||
CreateOutgoingWebhook(ctx context.Context, hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.Response, error)
|
||||
UpdateOutgoingWebhook(ctx context.Context, hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.Response, error)
|
||||
GetOutgoingWebhooks(ctx context.Context, page int, perPage int, etag string) ([]*model.OutgoingWebhook, *model.Response, error)
|
||||
GetOutgoingWebhook(ctx context.Context, hookID string) (*model.OutgoingWebhook, *model.Response, error)
|
||||
GetOutgoingWebhooksForChannel(ctx context.Context, channelID string, page int, perPage int, etag string) ([]*model.OutgoingWebhook, *model.Response, error)
|
||||
GetOutgoingWebhooksForTeam(ctx context.Context, teamID string, page int, perPage int, etag string) ([]*model.OutgoingWebhook, *model.Response, error)
|
||||
RegenOutgoingHookToken(ctx context.Context, hookID string) (*model.OutgoingWebhook, *model.Response, error)
|
||||
DeleteOutgoingWebhook(ctx context.Context, hookID string) (*model.Response, error)
|
||||
ListExports(ctx context.Context) ([]string, *model.Response, error)
|
||||
DeleteExport(ctx context.Context, name string) (*model.Response, error)
|
||||
DownloadExport(ctx context.Context, name string, wr io.Writer, offset int64) (int64, *model.Response, error)
|
||||
DownloadComplianceExport(ctx context.Context, jobID string, wr io.Writer) (string, error)
|
||||
GeneratePresignedURL(ctx context.Context, name string) (*model.PresignURLResponse, *model.Response, error)
|
||||
ResetSamlAuthDataToEmail(ctx context.Context, includeDeleted bool, dryRun bool, userIDs []string) (int64, *model.Response, error)
|
||||
GenerateSupportPacket(ctx context.Context) (io.ReadCloser, string, *model.Response, error)
|
||||
GetOAuthApps(ctx context.Context, page, perPage int) ([]*model.OAuthApp, *model.Response, error)
|
||||
GetPreferences(ctx context.Context, userId string) (model.Preferences, *model.Response, error)
|
||||
GetPreferencesByCategory(ctx context.Context, userId, category string) (model.Preferences, *model.Response, error)
|
||||
GetPreferenceByCategoryAndName(ctx context.Context, userId, category, preferenceName string) (*model.Preference, *model.Response, error)
|
||||
UpdatePreferences(ctx context.Context, userId string, preferences model.Preferences) (*model.Response, error)
|
||||
DeletePreferences(ctx context.Context, userId string, preferences model.Preferences) (*model.Response, error)
|
||||
PermanentDeletePost(ctx context.Context, postID string) (*model.Response, error)
|
||||
DeletePost(ctx context.Context, postId string) (*model.Response, error)
|
||||
ListCPAFields(ctx context.Context) ([]*model.PropertyField, *model.Response, error)
|
||||
CreateCPAField(ctx context.Context, field *model.PropertyField) (*model.PropertyField, *model.Response, error)
|
||||
PatchCPAField(ctx context.Context, fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.Response, error)
|
||||
DeleteCPAField(ctx context.Context, fieldID string) (*model.Response, error)
|
||||
ListCPAValues(ctx context.Context, userID string) (map[string]json.RawMessage, *model.Response, error)
|
||||
PatchCPAValues(ctx context.Context, values map[string]json.RawMessage) (map[string]json.RawMessage, *model.Response, error)
|
||||
PatchCPAValuesForUser(ctx context.Context, userID string, values map[string]json.RawMessage) (map[string]json.RawMessage, *model.Response, error)
|
||||
GetPostsForReporting(ctx context.Context, options model.ReportPostOptions, cursor model.ReportPostOptionsCursor) (*model.ReportPostListResponse, *model.Response, error)
|
||||
}
|
||||
447
server/cmd/mmctl/commands/auth.go
Normal file
447
server/cmd/mmctl/commands/auth.go
Normal file
@@ -0,0 +1,447 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
var AuthCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Manages the credentials of the remote Mattermost instances",
|
||||
}
|
||||
|
||||
var LoginCmd = &cobra.Command{
|
||||
Use: "login [instance url] --name [server name] --username [username] --password-file [password-file]",
|
||||
Short: "Login into an instance",
|
||||
Long: "Login into an instance and store credentials",
|
||||
Example: ` auth login https://mattermost.example.com
|
||||
auth login https://mattermost.example.com --name local-server --username sysadmin --password-file mysupersecret.txt
|
||||
auth login https://mattermost.example.com --name local-server --username sysadmin --password-file mysupersecret.txt --mfa-token 123456
|
||||
auth login https://mattermost.example.com --name local-server --access-token myaccesstoken`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: loginCmdF,
|
||||
}
|
||||
|
||||
var CurrentCmd = &cobra.Command{
|
||||
Use: "current",
|
||||
Short: "Show current user credentials",
|
||||
Long: "Show the currently stored user credentials",
|
||||
Example: ` auth current`,
|
||||
RunE: currentCmdF,
|
||||
}
|
||||
|
||||
var SetCmd = &cobra.Command{
|
||||
Use: "set [server name]",
|
||||
Short: "Set the credentials to use",
|
||||
Long: "Set an credentials to use in the following commands",
|
||||
Example: ` auth set local-server`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: setCmdF,
|
||||
}
|
||||
|
||||
var ListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Lists the credentials",
|
||||
Long: "Print a list of the registered credentials",
|
||||
Example: ` auth list`,
|
||||
RunE: listCmdF,
|
||||
}
|
||||
|
||||
var RenewCmd = &cobra.Command{
|
||||
Use: "renew",
|
||||
Short: "Renews a set of credentials",
|
||||
Long: "Renews the credentials for a given server",
|
||||
Example: ` auth renew local-server`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: renewCmdF,
|
||||
}
|
||||
|
||||
var DeleteCmd = &cobra.Command{
|
||||
Use: "delete [server name]",
|
||||
Short: "Delete an credentials",
|
||||
Long: "Delete an credentials by its name",
|
||||
Example: ` auth delete local-server`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: deleteCmdF,
|
||||
}
|
||||
|
||||
var CleanCmd = &cobra.Command{
|
||||
Use: "clean",
|
||||
Short: "Clean all credentials",
|
||||
Long: "Clean the currently stored credentials",
|
||||
Example: ` auth clean`,
|
||||
RunE: cleanCmdF,
|
||||
}
|
||||
|
||||
func init() {
|
||||
LoginCmd.Flags().StringP("name", "n", "", "Name for the credentials")
|
||||
LoginCmd.Flags().StringP("username", "u", "", "Username for the credentials")
|
||||
LoginCmd.Flags().StringP("access-token", "a", "", "Access token to use instead of username/password")
|
||||
_ = LoginCmd.Flags().MarkHidden("access-token")
|
||||
LoginCmd.Flags().StringP("access-token-file", "t", "", "Access token file to be read to use instead of username/password")
|
||||
LoginCmd.Flags().StringP("mfa-token", "m", "", "MFA token for the credentials")
|
||||
LoginCmd.Flags().StringP("password", "p", "", "Password for the credentials")
|
||||
_ = LoginCmd.Flags().MarkHidden("password")
|
||||
LoginCmd.Flags().StringP("password-file", "f", "", "Password file to be read for the credentials")
|
||||
LoginCmd.Flags().Bool("no-activate", false, "If present, it won't activate the credentials after login")
|
||||
|
||||
RenewCmd.Flags().StringP("password", "p", "", "Password for the credentials")
|
||||
_ = RenewCmd.Flags().MarkHidden("password")
|
||||
RenewCmd.Flags().StringP("password-file", "f", "", "Password file to be read for the credentials")
|
||||
RenewCmd.Flags().StringP("access-token", "a", "", "Access token to use instead of username/password")
|
||||
_ = RenewCmd.Flags().MarkHidden("access-token")
|
||||
RenewCmd.Flags().StringP("access-token-file", "t", "", "Access token file to be read to use instead of username/password")
|
||||
RenewCmd.Flags().StringP("mfa-token", "m", "", "MFA token for the credentials")
|
||||
|
||||
AuthCmd.AddCommand(
|
||||
LoginCmd,
|
||||
CurrentCmd,
|
||||
SetCmd,
|
||||
ListCmd,
|
||||
RenewCmd,
|
||||
DeleteCmd,
|
||||
CleanCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(AuthCmd)
|
||||
}
|
||||
|
||||
func loginCmdF(cmd *cobra.Command, args []string) error {
|
||||
name, err := cmd.Flags().GetString("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
username, err := cmd.Flags().GetString("username")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
password, err := cmd.Flags().GetString("password")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passwordFile, _ := cmd.Flags().GetString("password-file")
|
||||
if password != "" && passwordFile != "" {
|
||||
return errors.New("cannot use two passwords at the same time")
|
||||
}
|
||||
if fErr := readSecretFromFile(passwordFile, &password); fErr != nil {
|
||||
return fmt.Errorf("could not read the password: %w", fErr)
|
||||
}
|
||||
|
||||
accessToken, err := cmd.Flags().GetString("access-token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessTokenFile, _ := cmd.Flags().GetString("access-token-file")
|
||||
if accessToken != "" && accessTokenFile != "" {
|
||||
return errors.New("cannot use two access tokens at the same time")
|
||||
}
|
||||
if fErr := readSecretFromFile(accessTokenFile, &accessToken); fErr != nil {
|
||||
return fmt.Errorf("could not read the access-token: %w", fErr)
|
||||
}
|
||||
|
||||
mfaToken, err := cmd.Flags().GetString("mfa-token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allowInsecureSHA1 := viper.GetBool("insecure-sha1-intermediate")
|
||||
allowInsecureTLS := viper.GetBool("insecure-tls-version")
|
||||
|
||||
instanceURL := strings.TrimRight(args[0], "/")
|
||||
_, err = url.ParseRequestURI(instanceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse the instance url: %w", err)
|
||||
}
|
||||
|
||||
res, err := http.Get(instanceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get instance status: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("instance status code is not 200: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
method := MethodPassword
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
if name == "" {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Printf("Connection name: ")
|
||||
name, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
}
|
||||
|
||||
if accessToken != "" && username != "" {
|
||||
return errors.New("you must use --access-token or --username, but not both")
|
||||
}
|
||||
|
||||
if accessToken == "" && username == "" {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Printf("Username: ")
|
||||
username, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
username = strings.TrimSpace(username)
|
||||
}
|
||||
|
||||
if username != "" && password == "" {
|
||||
fmt.Printf("Password: ")
|
||||
stdinPassword, err := getPasswordFromStdin()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "couldn't read password")
|
||||
}
|
||||
password = stdinPassword
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
var c *model.Client4
|
||||
var err error
|
||||
if mfaToken != "" {
|
||||
c, _, err = InitClientWithMFA(ctx, username, password, mfaToken, instanceURL, allowInsecureSHA1, allowInsecureTLS)
|
||||
method = MethodMFA
|
||||
} else {
|
||||
c, _, err = InitClientWithUsernameAndPassword(ctx, username, password, instanceURL, allowInsecureSHA1, allowInsecureTLS)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initiate client: %w", err)
|
||||
}
|
||||
accessToken = c.AuthToken
|
||||
} else {
|
||||
username = "Personal Access Token"
|
||||
method = MethodToken
|
||||
credentials := Credentials{
|
||||
InstanceURL: instanceURL,
|
||||
AuthToken: accessToken,
|
||||
}
|
||||
if _, _, err := InitClientWithCredentials(ctx, &credentials, allowInsecureSHA1, allowInsecureTLS); err != nil {
|
||||
return fmt.Errorf("could not initiate client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
credentials := Credentials{
|
||||
Name: name,
|
||||
InstanceURL: instanceURL,
|
||||
Username: username,
|
||||
AuthToken: accessToken,
|
||||
AuthMethod: method,
|
||||
}
|
||||
|
||||
if err := SaveCredentials(credentials); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
noActivate, _ := cmd.Flags().GetBool("no-activate")
|
||||
if !noActivate {
|
||||
if err := SetCurrent(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("\n credentials for %q: \"%s@%s\" stored\n", name, username, instanceURL))
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPasswordFromStdin() (string, error) {
|
||||
// syscall.Stdin is of type int in all architectures but in
|
||||
// windows, so we have to cast it to ensure cross compatibility
|
||||
//nolint:unconvert
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println("")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytePassword), nil
|
||||
}
|
||||
|
||||
func currentCmdF(cmd *cobra.Command, args []string) error {
|
||||
credentials, err := GetCurrentCredentials()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("\n found credentials for %q: \"%s@%s\"\n", credentials.Name, credentials.Username, credentials.InstanceURL))
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCmdF(cmd *cobra.Command, args []string) error {
|
||||
if err := SetCurrent(args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("Credentials for server %q set as active", args[0]))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listCmdF(cmd *cobra.Command, args []string) error {
|
||||
credentialsList, err := ReadCredentialsList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(*credentialsList) == 0 {
|
||||
return errors.New("there are no registered credentials, maybe you need to use login first")
|
||||
}
|
||||
|
||||
serverNames := []string{}
|
||||
nameTitle, usernameTitle, instanceURLTitle := "Name", "Username", "InstanceURL"
|
||||
maxNameLen, maxUsernameLen, maxInstanceURLLen := len(nameTitle), len(usernameTitle), len(instanceURLTitle)
|
||||
for _, c := range *credentialsList {
|
||||
serverNames = append(serverNames, c.Name)
|
||||
if maxNameLen <= len(c.Name) {
|
||||
maxNameLen = len(c.Name)
|
||||
}
|
||||
if maxUsernameLen <= len(c.Username) {
|
||||
maxUsernameLen = len(c.Username)
|
||||
}
|
||||
if maxInstanceURLLen <= len(c.InstanceURL) {
|
||||
maxInstanceURLLen = len(c.InstanceURL)
|
||||
}
|
||||
}
|
||||
slices.Sort(serverNames)
|
||||
|
||||
printer.Print(fmt.Sprintf("\n | Active | %*s | %*s | %*s |", maxNameLen, nameTitle, maxUsernameLen, usernameTitle, maxInstanceURLLen, instanceURLTitle))
|
||||
printer.Print(fmt.Sprintf(" |%s|%s|%s|%s|", strings.Repeat("-", 8), strings.Repeat("-", maxNameLen+2), strings.Repeat("-", maxUsernameLen+2), strings.Repeat("-", maxInstanceURLLen+2)))
|
||||
for _, name := range serverNames {
|
||||
c := (*credentialsList)[name]
|
||||
if c.Active {
|
||||
printer.Print(fmt.Sprintf(" | * | %*s | %*s | %*s |", maxNameLen, c.Name, maxUsernameLen, c.Username, maxInstanceURLLen, c.InstanceURL))
|
||||
} else {
|
||||
printer.Print(fmt.Sprintf(" | | %*s | %*s | %*s |", maxNameLen, c.Name, maxUsernameLen, c.Username, maxInstanceURLLen, c.InstanceURL))
|
||||
}
|
||||
}
|
||||
printer.Print("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func renewCmdF(cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
passwordFile, _ := cmd.Flags().GetString("password-file")
|
||||
if password != "" && passwordFile != "" {
|
||||
return errors.New("cannot use two passwords at the same time")
|
||||
}
|
||||
if fErr := readSecretFromFile(passwordFile, &password); fErr != nil {
|
||||
return fmt.Errorf("could not read the password: %w", fErr)
|
||||
}
|
||||
|
||||
accessToken, _ := cmd.Flags().GetString("access-token")
|
||||
accessTokenFile, _ := cmd.Flags().GetString("access-token-file")
|
||||
if accessToken != "" && accessTokenFile != "" {
|
||||
return errors.New("cannot use two access tokens at the same time")
|
||||
}
|
||||
if fErr := readSecretFromFile(accessTokenFile, &accessToken); fErr != nil {
|
||||
return fmt.Errorf("could not read the access-token: %w", fErr)
|
||||
}
|
||||
|
||||
mfaToken, _ := cmd.Flags().GetString("mfa-token")
|
||||
allowInsecureSHA1 := viper.GetBool("insecure-sha1-intermediate")
|
||||
allowInsecureTLS := viper.GetBool("insecure-tls-version")
|
||||
|
||||
credentials, err := GetCredentials(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
if (credentials.AuthMethod == MethodPassword || credentials.AuthMethod == MethodMFA) && password == "" {
|
||||
if password == "" {
|
||||
fmt.Printf("Password: ")
|
||||
stdinPassword, err := getPasswordFromStdin()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "couldn't read password")
|
||||
}
|
||||
password = stdinPassword
|
||||
}
|
||||
}
|
||||
|
||||
switch credentials.AuthMethod {
|
||||
case MethodPassword:
|
||||
c, _, err := InitClientWithUsernameAndPassword(ctx, credentials.Username, password, credentials.InstanceURL, allowInsecureSHA1, allowInsecureTLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credentials.AuthToken = c.AuthToken
|
||||
|
||||
case MethodToken:
|
||||
if accessToken == "" {
|
||||
return errors.New("requires the --access-token parameter to be set")
|
||||
}
|
||||
|
||||
credentials.AuthToken = accessToken
|
||||
if _, _, err := InitClientWithCredentials(ctx, credentials, allowInsecureSHA1, allowInsecureTLS); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodMFA:
|
||||
if mfaToken == "" {
|
||||
return errors.New("requires the --mfa-token parameter to be set")
|
||||
}
|
||||
|
||||
c, _, err := InitClientWithMFA(ctx, credentials.Username, password, mfaToken, credentials.InstanceURL, allowInsecureSHA1, allowInsecureTLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
credentials.AuthToken = c.AuthToken
|
||||
|
||||
default:
|
||||
return errors.Errorf("invalid auth method %q", credentials.AuthMethod)
|
||||
}
|
||||
|
||||
if err := SaveCredentials(*credentials); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.PrintT("Credentials for server \"{{.Name}}\" successfully renewed", credentials)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteCmdF(cmd *cobra.Command, args []string) error {
|
||||
credentialsList, err := ReadCredentialsList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
credentials := (*credentialsList)[name]
|
||||
if credentials == nil {
|
||||
return errors.Errorf("cannot find credentials for server name %q", name)
|
||||
}
|
||||
|
||||
delete(*credentialsList, name)
|
||||
return SaveCredentialsList(credentialsList)
|
||||
}
|
||||
|
||||
func cleanCmdF(cmd *cobra.Command, args []string) error {
|
||||
if err := CleanCredentials(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
35
server/cmd/mmctl/commands/auth_e2e_test.go
Normal file
35
server/cmd/mmctl/commands/auth_e2e_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestAuthLoginWithTrailingSlashInInstanceURL() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("URL with trailing slash", func() {
|
||||
// loginCmdf doesn't return an error in this case. It prints to stderr instead.
|
||||
printer.Clean()
|
||||
|
||||
// cobra wont let us run a subcommand directly. When we try to `Execute`
|
||||
// the subcommand, cobra executes the parent command.
|
||||
// Instead of calling RootCmd, with its various subcommands and options,
|
||||
// we duplicate part of the LoginCmd here.
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringP("name", "n", "name", "Name for the credentials")
|
||||
cmd.Flags().StringP("username", "u", s.th.BasicUser.Username, "Username for the credentials")
|
||||
cmd.Flags().StringP("password", "p", s.th.BasicUser.Password, "Password for the credentials")
|
||||
cmd.Flags().StringP("access-token", "a", "", "Access token to use instead of username/password")
|
||||
cmd.Flags().StringP("mfa-token", "m", "", "MFA token for the credentials")
|
||||
cmd.Flags().Bool("no-activate", false, "If present, it won't activate the credentials after login")
|
||||
|
||||
_ = loginCmdF(cmd, []string{s.th.Client.URL + "/"}) // add a trailing slash
|
||||
errLines := printer.GetErrorLines()
|
||||
s.Require().Lenf(errLines, 0, "expected no error, got %q", errLines)
|
||||
})
|
||||
}
|
||||
88
server/cmd/mmctl/commands/auth_test.go
Normal file
88
server/cmd/mmctl/commands/auth_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestAuthList() {
|
||||
originalUser := *currentUser
|
||||
defer func() {
|
||||
SetUser(&originalUser)
|
||||
}()
|
||||
|
||||
tmp, errMkDir := os.MkdirTemp("", "mmctl-")
|
||||
s.Require().NoError(errMkDir)
|
||||
s.T().Cleanup(func() {
|
||||
s.Require().NoError(os.RemoveAll(tmp))
|
||||
})
|
||||
|
||||
testUser, err := user.Current()
|
||||
s.Require().NoError(err)
|
||||
testUser.HomeDir = tmp
|
||||
SetUser(testUser)
|
||||
viper.Set("config", filepath.Join(xdgConfigHomeVar, configParent, configFileName))
|
||||
|
||||
s.Run("short name", func() {
|
||||
printer.Clean()
|
||||
|
||||
credentials := Credentials{
|
||||
Name: "MyN",
|
||||
Username: "Un",
|
||||
AuthToken: "",
|
||||
AuthMethod: "",
|
||||
InstanceURL: "IURL",
|
||||
Active: true,
|
||||
}
|
||||
|
||||
err = CleanCredentials()
|
||||
s.Require().NoError(err)
|
||||
err = SaveCredentials(credentials)
|
||||
s.Require().NoError(err)
|
||||
|
||||
errListCmdF := listCmdF(&cobra.Command{}, []string{})
|
||||
s.Require().NoError(errListCmdF)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
lines := printer.GetLines()
|
||||
s.Require().Len(lines, 4)
|
||||
s.Require().Contains(lines[2], "| * | MyN | Un | IURL |")
|
||||
s.Require().Contains(lines[1], "|--------|------|----------|-------------|")
|
||||
s.Require().Contains(lines[0], "| Active | Name | Username | InstanceURL |")
|
||||
})
|
||||
|
||||
s.Run("normal name", func() {
|
||||
printer.Clean()
|
||||
|
||||
credentials := Credentials{
|
||||
Name: "MyName",
|
||||
Username: "MyUsername",
|
||||
AuthToken: "",
|
||||
AuthMethod: "",
|
||||
InstanceURL: "My Instance URL",
|
||||
Active: true,
|
||||
}
|
||||
|
||||
err = CleanCredentials()
|
||||
s.Require().NoError(err)
|
||||
err = SaveCredentials(credentials)
|
||||
s.Require().NoError(err)
|
||||
|
||||
errListCmdF := listCmdF(&cobra.Command{}, []string{})
|
||||
s.Require().NoError(errListCmdF)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
lines := printer.GetLines()
|
||||
s.Require().Len(lines, 4)
|
||||
s.Require().Contains(lines[2], "| * | MyName | MyUsername | My Instance URL |")
|
||||
s.Require().Contains(lines[1], "|--------|--------|------------|-----------------|")
|
||||
s.Require().Contains(lines[0], "| Active | Name | Username | InstanceURL |")
|
||||
})
|
||||
}
|
||||
195
server/cmd/mmctl/commands/auth_utils.go
Normal file
195
server/cmd/mmctl/commands/auth_utils.go
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
MethodPassword = "P"
|
||||
MethodToken = "T"
|
||||
MethodMFA = "M"
|
||||
|
||||
userHomeVar = "$HOME"
|
||||
configFileName = "config"
|
||||
configParent = "mmctl"
|
||||
xdgConfigHomeVar = "$XDG_CONFIG_HOME"
|
||||
)
|
||||
|
||||
type Credentials struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
AuthToken string `json:"authToken"`
|
||||
AuthMethod string `json:"authMethod"`
|
||||
InstanceURL string `json:"instanceUrl"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type CredentialsList map[string]*Credentials
|
||||
|
||||
var currentUser *user.User
|
||||
|
||||
func init() {
|
||||
newUser, err := user.Current()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
SetUser(newUser)
|
||||
}
|
||||
|
||||
func getDefaultConfigHomePath() string {
|
||||
if p, ok := os.LookupEnv(strings.TrimPrefix(xdgConfigHomeVar, "$")); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
return filepath.Join(currentUser.HomeDir, ".config")
|
||||
}
|
||||
|
||||
func resolveConfigFilePath() string {
|
||||
// resolve env vars if there are any
|
||||
fpath := strings.Replace(viper.GetString("config"), userHomeVar, currentUser.HomeDir, 1)
|
||||
|
||||
return strings.Replace(fpath, xdgConfigHomeVar, getDefaultConfigHomePath(), 1)
|
||||
}
|
||||
|
||||
func ReadCredentialsList() (*CredentialsList, error) {
|
||||
configPath := resolveConfigFilePath()
|
||||
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
return nil, errors.WithMessage(err, "cannot read user credentials, maybe you need to use login first")
|
||||
}
|
||||
|
||||
fileContents, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "there was a problem reading the credentials file")
|
||||
}
|
||||
|
||||
var credentialsList CredentialsList
|
||||
if err := json.Unmarshal(fileContents, &credentialsList); err != nil {
|
||||
return nil, errors.WithMessage(err, "there was a problem parsing the credentials file")
|
||||
}
|
||||
|
||||
return &credentialsList, nil
|
||||
}
|
||||
|
||||
func GetCurrentCredentials() (*Credentials, error) {
|
||||
credentialsList, err := ReadCredentialsList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range *credentialsList {
|
||||
if c.Active {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.Errorf("no current context available. please use the %q command", "auth set")
|
||||
}
|
||||
|
||||
func GetCredentials(name string) (*Credentials, error) {
|
||||
credentialsList, err := ReadCredentialsList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range *credentialsList {
|
||||
if c.Name == name {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.Errorf("couldn't find credentials for connection %q", name)
|
||||
}
|
||||
|
||||
func SaveCredentials(credentials Credentials) error {
|
||||
credentialsList, err := ReadCredentialsList()
|
||||
if err != nil {
|
||||
// we get the parent of the file so that we can create the path if it doesn't exist.
|
||||
configParent := filepath.Dir(resolveConfigFilePath())
|
||||
if err := os.MkdirAll(configParent, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
credentialsList = &CredentialsList{}
|
||||
credentials.Active = true
|
||||
}
|
||||
|
||||
(*credentialsList)[credentials.Name] = &credentials
|
||||
return SaveCredentialsList(credentialsList)
|
||||
}
|
||||
|
||||
func SaveCredentialsList(credentialsList *CredentialsList) error {
|
||||
configPath := resolveConfigFilePath()
|
||||
|
||||
marshaledCredentialsList, _ := json.MarshalIndent(credentialsList, "", " ")
|
||||
|
||||
if err := os.WriteFile(configPath, marshaledCredentialsList, 0600); err != nil {
|
||||
return errors.WithMessage(err, "cannot save the credentials")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetCurrent(name string) error {
|
||||
credentialsList, err := ReadCredentialsList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, c := range *credentialsList {
|
||||
if c.Name == name {
|
||||
found = true
|
||||
c.Active = true
|
||||
} else {
|
||||
c.Active = false
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return errors.Errorf("cannot find credentials for server %q", name)
|
||||
}
|
||||
|
||||
return SaveCredentialsList(credentialsList)
|
||||
}
|
||||
|
||||
func CleanCredentials() error {
|
||||
configFilePath := resolveConfigFilePath()
|
||||
|
||||
if _, err := os.Stat(configFilePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(configFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetUser(newUser *user.User) {
|
||||
currentUser = newUser
|
||||
}
|
||||
|
||||
// will read the scret from file, if there is one
|
||||
func readSecretFromFile(file string, secret *string) error {
|
||||
if file != "" {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*secret = strings.TrimSpace(string(b))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
176
server/cmd/mmctl/commands/auth_utils_test.go
Normal file
176
server/cmd/mmctl/commands/auth_utils_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveConfigFilePath(t *testing.T) {
|
||||
originalUser := *currentUser
|
||||
defer func() {
|
||||
SetUser(&originalUser)
|
||||
}()
|
||||
|
||||
testUser, err := user.Current()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should return the default config file location if nothing else is set", func(t *testing.T) {
|
||||
tmp, err := os.MkdirTemp("", "mmctl-")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(tmp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
testUser.HomeDir = tmp
|
||||
SetUser(testUser)
|
||||
|
||||
expected := filepath.Join(getDefaultConfigHomePath(), configParent, configFileName)
|
||||
|
||||
viper.Set("config", filepath.Join(xdgConfigHomeVar, configParent, configFileName))
|
||||
|
||||
p := resolveConfigFilePath()
|
||||
require.Equal(t, expected, p)
|
||||
})
|
||||
|
||||
t.Run("should return config file location from xdg environment variable", func(t *testing.T) {
|
||||
tmp, err := os.MkdirTemp("", "mmctl-")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(tmp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
testUser.HomeDir = tmp
|
||||
SetUser(testUser)
|
||||
|
||||
expected := filepath.Join(testUser.HomeDir, ".config", configParent, configFileName)
|
||||
|
||||
t.Setenv("XDG_CONFIG_HOME", filepath.Join(testUser.HomeDir, ".config"))
|
||||
viper.Set("config", filepath.Join(xdgConfigHomeVar, configParent, configFileName))
|
||||
|
||||
p := resolveConfigFilePath()
|
||||
require.Equal(t, expected, p)
|
||||
})
|
||||
|
||||
t.Run("should return the user-defined config file path if one is set", func(t *testing.T) {
|
||||
tmp, err := os.MkdirTemp("", "mmctl-")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(tmp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
testUser.HomeDir = tmp + "/path/should/be/ignored"
|
||||
SetUser(testUser)
|
||||
|
||||
expected := filepath.Join(tmp, configFileName)
|
||||
|
||||
t.Setenv("XDG_CONFIG_HOME", "path/should/be/ignored")
|
||||
viper.Set("config", expected)
|
||||
|
||||
p := resolveConfigFilePath()
|
||||
require.Equal(t, expected, p)
|
||||
})
|
||||
|
||||
t.Run("should resolve config file path if $HOME variable is used", func(t *testing.T) {
|
||||
tmp, err := os.MkdirTemp("", "mmctl-")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(tmp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
testUser.HomeDir = tmp + "/path/should/be/ignored"
|
||||
SetUser(testUser)
|
||||
|
||||
expected := filepath.Join(testUser.HomeDir, "/.config/mmctl/config")
|
||||
|
||||
t.Setenv("XDG_CONFIG_HOME", "path/should/be/ignored")
|
||||
viper.Set("config", "$HOME/.config/mmctl/config")
|
||||
|
||||
p := resolveConfigFilePath()
|
||||
require.Equal(t, expected, p)
|
||||
})
|
||||
|
||||
t.Run("should create the user-defined config file path if one is set", func(t *testing.T) {
|
||||
tmp, err := os.MkdirTemp("", "mmctl-")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(tmp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
testUser.HomeDir = tmp + "path/should/be/ignored"
|
||||
SetUser(testUser)
|
||||
extraDir := "extra"
|
||||
|
||||
expected := filepath.Join(tmp, extraDir, "config.json")
|
||||
|
||||
t.Setenv("XDG_CONFIG_HOME", "path/should/be/ignored")
|
||||
viper.Set("config", expected)
|
||||
|
||||
err = SaveCredentials(Credentials{})
|
||||
require.NoError(t, err)
|
||||
info, err := os.Stat(expected)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, info.IsDir())
|
||||
assert.True(t, info.Name() == "config.json")
|
||||
})
|
||||
|
||||
t.Run("should return error if the config flag is set to a directory", func(t *testing.T) {
|
||||
tmp, err := os.MkdirTemp("", "mmctl-")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(tmp)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
testUser.HomeDir = tmp + "path/should/be/ignored"
|
||||
SetUser(testUser)
|
||||
|
||||
t.Setenv("XDG_CONFIG_HOME", "path/should/be/ignored")
|
||||
viper.Set("config", tmp)
|
||||
|
||||
err = SaveCredentials(Credentials{})
|
||||
require.Error(t, err)
|
||||
require.True(t, strings.HasSuffix(err.Error(), "is a directory"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadSecretFromFile(t *testing.T) {
|
||||
f, err := os.CreateTemp(t.TempDir(), "mmctl")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.WriteString("test-pass")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("password from file", func(t *testing.T) {
|
||||
var pass string
|
||||
err := readSecretFromFile(f.Name(), &pass)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-pass", pass)
|
||||
})
|
||||
|
||||
t.Run("no file path is provided", func(t *testing.T) {
|
||||
pass := "test-pass-2"
|
||||
err := readSecretFromFile("", &pass)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-pass-2", pass)
|
||||
})
|
||||
|
||||
t.Run("nonexistent file provided", func(t *testing.T) {
|
||||
var pass string
|
||||
err := readSecretFromFile(filepath.Join(t.TempDir(), "bla"), &pass)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, pass)
|
||||
})
|
||||
}
|
||||
285
server/cmd/mmctl/commands/bot.go
Normal file
285
server/cmd/mmctl/commands/bot.go
Normal file
@@ -0,0 +1,285 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var BotCmd = &cobra.Command{
|
||||
Use: "bot",
|
||||
Short: "Management of bots",
|
||||
}
|
||||
|
||||
var CreateBotCmd = &cobra.Command{
|
||||
Use: "create [username]",
|
||||
Short: "Create bot",
|
||||
Long: "Create bot.",
|
||||
Example: ` bot create testbot`,
|
||||
PreRun: disableLocalPrecheck,
|
||||
RunE: withClient(botCreateCmdF),
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
var UpdateBotCmd = &cobra.Command{
|
||||
Use: "update [username]",
|
||||
Short: "Update bot",
|
||||
Long: "Update bot information.",
|
||||
Example: ` bot update testbot --username newbotusername`,
|
||||
RunE: withClient(botUpdateCmdF),
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
var ListBotCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List bots",
|
||||
Long: "List the bots users.",
|
||||
Example: ` bot list`,
|
||||
RunE: withClient(botListCmdF),
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
var DisableBotCmd = &cobra.Command{
|
||||
Use: "disable [username]",
|
||||
Short: "Disable bot",
|
||||
Long: "Disable an enabled bot",
|
||||
Example: ` bot disable testbot`,
|
||||
RunE: withClient(botDisableCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
|
||||
var EnableBotCmd = &cobra.Command{
|
||||
Use: "enable [username]",
|
||||
Short: "Enable bot",
|
||||
Long: "Enable a disabled bot",
|
||||
Example: ` bot enable testbot`,
|
||||
RunE: withClient(botEnableCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
|
||||
var AssignBotCmd = &cobra.Command{
|
||||
Use: "assign [bot-username] [new-owner-username]",
|
||||
Short: "Assign bot",
|
||||
Long: "Assign the ownership of a bot to another user",
|
||||
Example: ` bot assign testbot user2`,
|
||||
RunE: withClient(botAssignCmdF),
|
||||
Args: cobra.ExactArgs(2),
|
||||
}
|
||||
|
||||
func init() {
|
||||
CreateBotCmd.Flags().String("display-name", "", "Optional. The display name for the new bot.")
|
||||
CreateBotCmd.Flags().String("description", "", "Optional. The description text for the new bot.")
|
||||
CreateBotCmd.Flags().Bool("with-token", false, "Optional. Auto genreate access token for the bot.")
|
||||
ListBotCmd.Flags().Bool("orphaned", false, "Optional. Only show orphaned bots.")
|
||||
ListBotCmd.Flags().Bool("all", false, "Optional. Show all bots (including deleleted and orphaned).")
|
||||
UpdateBotCmd.Flags().String("username", "", "Optional. The new username for the bot.")
|
||||
UpdateBotCmd.Flags().String("display-name", "", "Optional. The new display name for the bot.")
|
||||
UpdateBotCmd.Flags().String("description", "", "Optional. The new description text for the bot.")
|
||||
|
||||
BotCmd.AddCommand(
|
||||
CreateBotCmd,
|
||||
UpdateBotCmd,
|
||||
ListBotCmd,
|
||||
EnableBotCmd,
|
||||
DisableBotCmd,
|
||||
AssignBotCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(BotCmd)
|
||||
}
|
||||
|
||||
func botCreateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
username := args[0]
|
||||
displayName, _ := cmd.Flags().GetString("display-name")
|
||||
description, _ := cmd.Flags().GetString("description")
|
||||
|
||||
bot, _, err := c.CreateBot(context.TODO(), &model.Bot{
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
Description: description,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Errorf("could not create bot: %s", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Created bot {{.UserId}}", bot)
|
||||
|
||||
if withToken, _ := cmd.Flags().GetBool("with-token"); withToken {
|
||||
return generateTokenForAUserCmdF(c, cmd, []string{args[0], "autogenerated"})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func botUpdateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if !cmd.Flags().Changed("username") && !cmd.Flags().Changed("display-name") && !cmd.Flags().Changed("description") {
|
||||
return errors.New("at least one of --username, --display-name or --description must be set")
|
||||
}
|
||||
|
||||
user := getUserFromUserArg(c, args[0])
|
||||
if user == nil {
|
||||
return errors.New("unable to find user '" + args[0] + "'")
|
||||
}
|
||||
patch := model.BotPatch{}
|
||||
username, err := cmd.Flags().GetString("username")
|
||||
if err == nil && cmd.Flags().Changed("username") {
|
||||
patch.Username = &username
|
||||
}
|
||||
displayName, err := cmd.Flags().GetString("display-name")
|
||||
if err == nil && cmd.Flags().Changed("display-name") {
|
||||
patch.DisplayName = &displayName
|
||||
}
|
||||
description, err := cmd.Flags().GetString("description")
|
||||
if err == nil && cmd.Flags().Changed("description") {
|
||||
patch.Description = &description
|
||||
}
|
||||
|
||||
bot, _, err := c.PatchBot(context.TODO(), user.Id, &patch)
|
||||
if err != nil {
|
||||
return errors.Errorf("could not update bot: %s", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Updated bot {{.UserId}} ({{.Username}})", bot)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func botListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
orphaned, _ := cmd.Flags().GetBool("orphaned")
|
||||
all, _ := cmd.Flags().GetBool("all")
|
||||
|
||||
page := 0
|
||||
perPage := DefaultPageSize
|
||||
tpl := `{{.UserId}}: {{.Username}}`
|
||||
for {
|
||||
var bots []*model.Bot
|
||||
var err error
|
||||
if all { //nolint:gocritic
|
||||
bots, _, err = c.GetBotsIncludeDeleted(context.TODO(), page, perPage, "")
|
||||
} else if orphaned {
|
||||
bots, _, err = c.GetBotsOrphaned(context.TODO(), page, perPage, "")
|
||||
} else {
|
||||
bots, _, err = c.GetBots(context.TODO(), page, perPage, "")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to fetch bots")
|
||||
}
|
||||
|
||||
userIds := []string{}
|
||||
for _, bot := range bots {
|
||||
userIds = append(userIds, bot.OwnerId)
|
||||
}
|
||||
|
||||
users, _, err := c.GetUsersByIds(context.TODO(), userIds)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to fetch bots")
|
||||
}
|
||||
|
||||
usersByID := map[string]*model.User{}
|
||||
for _, user := range users {
|
||||
usersByID[user.Id] = user
|
||||
}
|
||||
|
||||
var ownerName string
|
||||
var ownerDeleteAt int64
|
||||
for _, bot := range bots {
|
||||
if owner, ok := usersByID[bot.OwnerId]; ok {
|
||||
ownerName = owner.Username
|
||||
ownerDeleteAt = owner.DeleteAt
|
||||
} else {
|
||||
// not all bots have a userId in their ownerId field.
|
||||
ownerName = bot.OwnerId
|
||||
ownerDeleteAt = 0
|
||||
}
|
||||
tplExtraText := fmt.Sprintf("(Owned by %s, {{if ne .DeleteAt 0}}Disabled{{else}}Enabled{{end}}{{if ne %d 0}}, Orphaned{{end}})", ownerName, ownerDeleteAt)
|
||||
printer.PrintT(tpl+tplExtraText, bot)
|
||||
}
|
||||
|
||||
if len(bots) < perPage {
|
||||
break
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func botEnableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
users := getUsersFromUserArgs(c, args)
|
||||
|
||||
var result *multierror.Error
|
||||
|
||||
for i, user := range users {
|
||||
if user == nil {
|
||||
printer.PrintError(fmt.Sprintf("can't find user '%v'", args[i]))
|
||||
result = multierror.Append(result, fmt.Errorf("can't find user %q", args[i]))
|
||||
continue
|
||||
}
|
||||
|
||||
bot, _, err := c.EnableBot(context.TODO(), user.Id)
|
||||
if err != nil {
|
||||
printer.PrintError(fmt.Sprintf("could not enable bot '%v'", args[i]))
|
||||
result = multierror.Append(result, fmt.Errorf("could not enable bot %q: %w", args[i], err))
|
||||
continue
|
||||
}
|
||||
|
||||
printer.PrintT("Enabled bot {{.UserId}} ({{.Username}})", bot)
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func botDisableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
users := getUsersFromUserArgs(c, args)
|
||||
|
||||
var result *multierror.Error
|
||||
for i, user := range users {
|
||||
if user == nil {
|
||||
printer.PrintError(fmt.Sprintf("can't find user '%v'", args[i]))
|
||||
result = multierror.Append(result, fmt.Errorf("can't find user %q", args[i]))
|
||||
continue
|
||||
}
|
||||
|
||||
bot, _, err := c.DisableBot(context.TODO(), user.Id)
|
||||
if err != nil {
|
||||
printer.PrintError(fmt.Sprintf("could not disable bot '%v'", args[i]))
|
||||
result = multierror.Append(result, fmt.Errorf("could not disable bot %q: %w", args[i], err))
|
||||
continue
|
||||
}
|
||||
|
||||
printer.PrintT("Disabled bot {{.UserId}} ({{.Username}})", bot)
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func botAssignCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
botUser := getUserFromUserArg(c, args[0])
|
||||
if botUser == nil {
|
||||
return errors.New("unable to find user '" + args[0] + "'")
|
||||
}
|
||||
newOwnerUser := getUserFromUserArg(c, args[1])
|
||||
if newOwnerUser == nil {
|
||||
return errors.New("unable to find user '" + args[1] + "'")
|
||||
}
|
||||
|
||||
newBot, _, err := c.AssignBot(context.TODO(), botUser.Id, newOwnerUser.Id)
|
||||
if err != nil {
|
||||
return errors.Errorf("can not assign bot '%s' to user '%s'", args[0], args[1])
|
||||
}
|
||||
|
||||
printer.PrintT("The bot {{.UserId}} ({{.Username}}) now belongs to the user "+newOwnerUser.Username, newBot)
|
||||
return nil
|
||||
}
|
||||
434
server/cmd/mmctl/commands/bot_e2e_test.go
Normal file
434
server/cmd/mmctl/commands/bot_e2e_test.go
Normal file
@@ -0,0 +1,434 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestListBotCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T()).DeleteBots(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("List Bot", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
bot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: s.th.BasicUser.Id})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, bot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
deletedBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: s.th.BasicUser.Id})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, deletedBot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
deletedBot, appErr = s.th.App.UpdateBotActive(s.th.Context, deletedBot.UserId, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := botListCmdF(c, &cobra.Command{}, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(1, len(printer.GetLines()))
|
||||
|
||||
listedBot, ok := printer.GetLines()[0].(*model.Bot)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(bot.Username, listedBot.Username)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("List Bot only orphaned", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId(), DeleteAt: 1})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteUser(s.th.Context, user)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
bot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: s.th.BasicUser.Id})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, bot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
deletedBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, deletedBot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
deletedBot, appErr = s.th.App.UpdateBotActive(s.th.Context, deletedBot.UserId, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
orphanBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, orphanBot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", true, "")
|
||||
|
||||
err := botListCmdF(c, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(1, len(printer.GetLines()))
|
||||
|
||||
listedBot, ok := printer.GetLines()[0].(*model.Bot)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(orphanBot.Username, listedBot.Username)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("List all Bots", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId(), DeleteAt: 1})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteUser(s.th.Context, user)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
bot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: s.th.BasicUser2.Id})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, bot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
orphanBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, orphanBot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
deletedBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: s.th.BasicUser2.Id})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, deletedBot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
deletedBot, appErr = s.th.App.UpdateBotActive(s.th.Context, deletedBot.UserId, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("all", true, "")
|
||||
|
||||
err := botListCmdF(c, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(3, len(printer.GetLines()))
|
||||
resultBot, ok := printer.GetLines()[0].(*model.Bot)
|
||||
s.True(ok)
|
||||
s.Require().Equal(bot, resultBot)
|
||||
resultOrphanBot, ok := printer.GetLines()[1].(*model.Bot)
|
||||
s.True(ok)
|
||||
s.Require().Equal(orphanBot, resultOrphanBot)
|
||||
resultDeletedBot, ok := printer.GetLines()[2].(*model.Bot)
|
||||
s.True(ok)
|
||||
s.Require().Equal(deletedBot, resultDeletedBot)
|
||||
})
|
||||
|
||||
s.Run("List Bots without permission", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := botListCmdF(s.th.Client, cmd, []string{})
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal("Failed to fetch bots: You do not have the appropriate permissions.", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestBotEnableCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableBotAccountCreation = true })
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForSystemAdminAndLocal("enable a bot", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
newBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, appErr = s.th.App.UpdateBotActive(s.th.Context, newBot.UserId, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := botEnableCmdF(c, &cobra.Command{}, []string{newBot.UserId})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
printedBot := printer.GetLines()[0].(*model.Bot)
|
||||
s.Require().Equal(newBot.UserId, printedBot.UserId)
|
||||
s.Require().Equal(newBot.Username, printedBot.Username)
|
||||
s.Require().Equal(newBot.OwnerId, printedBot.OwnerId)
|
||||
|
||||
bot, appErr := s.th.App.GetBot(s.th.Context, newBot.UserId, false)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal(newBot.UserId, bot.UserId)
|
||||
s.Require().Equal(newBot.Username, bot.Username)
|
||||
s.Require().Equal(newBot.OwnerId, bot.OwnerId)
|
||||
})
|
||||
|
||||
s.Run("enable a bot without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
newBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, appErr = s.th.App.UpdateBotActive(s.th.Context, newBot.UserId, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := botEnableCmdF(s.th.Client, &cobra.Command{}, []string{newBot.UserId})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "could not enable bot")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("enable a nonexistent bot", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := botEnableCmdF(c, &cobra.Command{}, []string{"nonexistent-bot-userid"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "can't find user 'nonexistent-bot-userid'")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("enable an already enabled bot", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
newBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, appErr = s.th.App.UpdateBotActive(s.th.Context, newBot.UserId, true)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := botEnableCmdF(c, &cobra.Command{}, []string{newBot.UserId})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
printedBot := printer.GetLines()[0].(*model.Bot)
|
||||
s.Require().Equal(newBot.UserId, printedBot.UserId)
|
||||
s.Require().Equal(newBot.Username, printedBot.Username)
|
||||
s.Require().Equal(newBot.OwnerId, printedBot.OwnerId)
|
||||
|
||||
bot, appErr := s.th.App.GetBot(s.th.Context, newBot.UserId, false)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal(newBot.UserId, bot.UserId)
|
||||
s.Require().Equal(newBot.Username, bot.Username)
|
||||
s.Require().Equal(newBot.OwnerId, bot.OwnerId)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestBotDisableCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableBotAccountCreation = true })
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForSystemAdminAndLocal("disable a bot", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
newBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, appErr = s.th.App.UpdateBotActive(s.th.Context, newBot.UserId, true)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := botDisableCmdF(c, &cobra.Command{}, []string{newBot.UserId})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
printedBot := printer.GetLines()[0].(*model.Bot)
|
||||
s.Require().Equal(newBot.UserId, printedBot.UserId)
|
||||
s.Require().Equal(newBot.Username, printedBot.Username)
|
||||
s.Require().Equal(newBot.OwnerId, printedBot.OwnerId)
|
||||
|
||||
_, appErr = s.th.App.GetBot(s.th.Context, newBot.UserId, false)
|
||||
s.Require().NotNil(appErr)
|
||||
s.Require().Equal("store.sql_bot.get.missing.app_error", appErr.Id)
|
||||
})
|
||||
|
||||
s.Run("disable a bot without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
newBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, appErr = s.th.App.UpdateBotActive(s.th.Context, newBot.UserId, true)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := botDisableCmdF(s.th.Client, &cobra.Command{}, []string{newBot.UserId})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "could not disable bot")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("disable a nonexistent bot", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := botDisableCmdF(c, &cobra.Command{}, []string{"nonexistent-bot-userid"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "can't find user 'nonexistent-bot-userid'")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("disable an already disabled bot", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
newBot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, appErr = s.th.App.UpdateBotActive(s.th.Context, newBot.UserId, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := botDisableCmdF(c, &cobra.Command{}, []string{newBot.UserId})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
printedBot := printer.GetLines()[0].(*model.Bot)
|
||||
s.Require().Equal(newBot.UserId, printedBot.UserId)
|
||||
s.Require().Equal(newBot.Username, printedBot.Username)
|
||||
s.Require().Equal(newBot.OwnerId, printedBot.OwnerId)
|
||||
|
||||
_, appErr = s.th.App.GetBot(s.th.Context, newBot.UserId, false)
|
||||
s.Require().NotNil(appErr)
|
||||
s.Require().Equal("store.sql_bot.get.missing.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestBotAssignCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("Assign Bot", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
botOwner, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteUser(s.th.Context, botOwner)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
newBotOwner, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteUser(s.th.Context, newBotOwner)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
bot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: botOwner.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal(bot.OwnerId, botOwner.Id)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, bot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
err := botAssignCmdF(c, &cobra.Command{}, []string{bot.UserId, newBotOwner.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(1, len(printer.GetLines()))
|
||||
newBot, ok := printer.GetLines()[0].(*model.Bot)
|
||||
s.True(ok)
|
||||
s.Require().Equal(newBotOwner.Id, newBot.OwnerId)
|
||||
})
|
||||
|
||||
s.Run("Assign Bot without permission", func() {
|
||||
printer.Clean()
|
||||
|
||||
botOwner, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteUser(s.th.Context, botOwner)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
newBotOwner, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteUser(s.th.Context, newBotOwner)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
bot, appErr := s.th.App.CreateBot(s.th.Context, &model.Bot{Username: model.NewUsername(), OwnerId: botOwner.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal(bot.OwnerId, botOwner.Id)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, bot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
err := botAssignCmdF(s.th.Client, &cobra.Command{}, []string{bot.UserId, newBotOwner.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, fmt.Sprintf(`can not assign bot '%s' to user '%s'`, bot.UserId, newBotOwner.Id), err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestBotCreateCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
createBots := *s.th.App.Config().ServiceSettings.EnableBotAccountCreation
|
||||
s.th.App.UpdateConfig(func(c *model.Config) { *c.ServiceSettings.EnableBotAccountCreation = true })
|
||||
defer s.th.App.UpdateConfig(func(c *model.Config) { *c.ServiceSettings.EnableBotAccountCreation = createBots })
|
||||
|
||||
s.Run("MM-T3941 Create Bot with an access token", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("with-token", true, "")
|
||||
|
||||
err := botCreateCmdF(s.th.Client, cmd, []string{"testbot"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
printer.Clean()
|
||||
|
||||
err = botCreateCmdF(s.th.SystemAdminClient, cmd, []string{"testbot"})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(2, len(printer.GetLines()))
|
||||
bot, ok := printer.GetLines()[0].(*model.Bot)
|
||||
s.Require().True(ok)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteBot(s.th.Context, bot.UserId)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
token, ok := printer.GetLines()[1].(*model.UserAccessToken)
|
||||
s.Require().True(ok)
|
||||
defer func() {
|
||||
err := s.th.App.RevokeUserAccessToken(s.th.Context, token)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
}
|
||||
670
server/cmd/mmctl/commands/bot_test.go
Normal file
670
server/cmd/mmctl/commands/bot_test.go
Normal file
@@ -0,0 +1,670 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestBotCreateCmd() {
|
||||
s.Run("Should create a bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("display-name", "some-name", "")
|
||||
cmd.Flags().String("description", "some-text", "")
|
||||
mockBot := model.Bot{Username: botArg, DisplayName: "some-name", Description: "some-text"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateBot(context.TODO(), &mockBot).
|
||||
Return(&mockBot, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botCreateCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should create a bot with an access token", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("display-name", "some-name", "")
|
||||
cmd.Flags().String("description", "some-text", "")
|
||||
cmd.Flags().Bool("with-token", true, "")
|
||||
mockBot := model.Bot{Username: botArg, DisplayName: "some-name", Description: "some-text"}
|
||||
mockToken := model.UserAccessToken{Token: "token-id", Description: "autogenerated"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateBot(context.TODO(), &mockBot).
|
||||
Return(&mockBot, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(model.UserFromBot(&mockBot), &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUserAccessToken(context.TODO(), mockBot.UserId, "autogenerated", int64(0)).
|
||||
Return(&mockToken, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botCreateCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
s.Require().Equal(&mockToken, printer.GetLines()[1])
|
||||
})
|
||||
|
||||
s.Run("Should error when creating a bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
mockBot := model.Bot{Username: botArg, DisplayName: "", Description: ""}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateBot(context.TODO(), &mockBot).
|
||||
Return(nil, &model.Response{}, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
err := botCreateCmdF(s.client, &cobra.Command{}, []string{botArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "could not create bot")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestBotUpdateCmd() {
|
||||
s.Run("Should update a bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("username", "new-username", "")
|
||||
cmd.Flags().String("display-name", "some-name", "")
|
||||
cmd.Flags().String("description", "some-text", "")
|
||||
cmd.Flags().Lookup("username").Changed = true
|
||||
cmd.Flags().Lookup("display-name").Changed = true
|
||||
cmd.Flags().Lookup("description").Changed = true
|
||||
mockBot := model.Bot{Username: "new-username", DisplayName: "some-name", Description: "some-text"}
|
||||
mockUser := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchBot(context.TODO(), mockUser.Id, gomock.Any()).
|
||||
Return(&mockBot, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botUpdateCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should error when user not found bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("username", "bot-username", "")
|
||||
cmd.Flags().Lookup("username").Changed = true
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), botArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botUpdateCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "unable to find user 'a-bot'")
|
||||
})
|
||||
|
||||
s.Run("Should error when updating bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("display-name", "some-name", "")
|
||||
cmd.Flags().String("description", "some-text", "")
|
||||
cmd.Flags().Lookup("display-name").Changed = true
|
||||
cmd.Flags().Lookup("description").Changed = true
|
||||
mockUser := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchBot(context.TODO(), mockUser.Id, gomock.Any()).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botUpdateCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "could not update bot")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestBotListCmd() {
|
||||
s.Run("Should list correctly all", func() {
|
||||
printer.Clean()
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", false, "")
|
||||
cmd.Flags().Bool("all", true, "")
|
||||
mockBot := model.Bot{UserId: model.NewId(), Username: botArg, DisplayName: "some-name", Description: "some-text", OwnerId: model.NewId()}
|
||||
mockUser := model.User{Id: mockBot.OwnerId}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetBotsIncludeDeleted(context.TODO(), 0, 200, "").
|
||||
Return([]*model.Bot{&mockBot}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockBot.OwnerId}).
|
||||
Return([]*model.User{&mockUser}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botListCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should list fail if one featching all bots requests fail", func() {
|
||||
printer.Clean()
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", false, "")
|
||||
cmd.Flags().Bool("all", true, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetBotsIncludeDeleted(context.TODO(), 0, 200, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botListCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "Failed to fetch bots")
|
||||
})
|
||||
|
||||
s.Run("Should list correctly orphaned", func() {
|
||||
printer.Clean()
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", true, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
mockBot := model.Bot{UserId: model.NewId(), Username: botArg, DisplayName: "some-name", Description: "some-text", OwnerId: model.NewId()}
|
||||
mockUser := model.User{Id: mockBot.OwnerId}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetBotsOrphaned(context.TODO(), 0, 200, "").
|
||||
Return([]*model.Bot{&mockBot}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockBot.OwnerId}).
|
||||
Return([]*model.User{&mockUser}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botListCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should list fail if one featching bots orphaned requests fail", func() {
|
||||
printer.Clean()
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", true, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetBotsOrphaned(context.TODO(), 0, 200, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botListCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "Failed to fetch bots")
|
||||
})
|
||||
|
||||
s.Run("Should list correctly bots", func() {
|
||||
printer.Clean()
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", false, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
mockBot := model.Bot{UserId: model.NewId(), Username: botArg, DisplayName: "some-name", Description: "some-text", OwnerId: model.NewId()}
|
||||
mockUser := model.User{Id: mockBot.OwnerId}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetBots(context.TODO(), 0, 200, "").
|
||||
Return([]*model.Bot{&mockBot}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockBot.OwnerId}).
|
||||
Return([]*model.User{&mockUser}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botListCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should list correctly bots with invalid ownerId", func() {
|
||||
printer.Clean()
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", false, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
mockBot := model.Bot{UserId: model.NewId(), Username: botArg, DisplayName: "some-name", Description: "some-text", OwnerId: "Mr.Robot"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetBots(context.TODO(), 0, 200, "").
|
||||
Return([]*model.Bot{&mockBot}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockBot.OwnerId}).
|
||||
Return([]*model.User{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botListCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should list fail if one fetching bots requests fail", func() {
|
||||
printer.Clean()
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", false, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetBots(context.TODO(), 0, 200, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botListCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "Failed to fetch bots")
|
||||
})
|
||||
|
||||
s.Run("Should list fail if fetching owners requests fail", func() {
|
||||
printer.Clean()
|
||||
botArg := "a-bot"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("orphaned", false, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
mockBot := model.Bot{UserId: model.NewId(), Username: botArg, DisplayName: "some-name", Description: "some-text", OwnerId: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetBots(context.TODO(), 0, 200, "").
|
||||
Return([]*model.Bot{&mockBot}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockBot.OwnerId}).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botListCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "Failed to fetch bots")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestBotDisableCmd() {
|
||||
s.Run("Should disable a bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
|
||||
mockBot := model.Bot{Username: botArg, DisplayName: "some-name", Description: "some-text"}
|
||||
mockUser := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DisableBot(context.TODO(), mockUser.Id).
|
||||
Return(&mockBot, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botDisableCmdF(s.client, &cobra.Command{}, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should error when user not found bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), botArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botDisableCmdF(s.client, &cobra.Command{}, []string{botArg})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "can't find user 'a-bot'")
|
||||
})
|
||||
|
||||
s.Run("Should error when disabling bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("display-name", "some-name", "")
|
||||
cmd.Flags().String("description", "some-text", "")
|
||||
cmd.Flags().Lookup("display-name").Changed = true
|
||||
cmd.Flags().Lookup("description").Changed = true
|
||||
mockUser := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DisableBot(context.TODO(), mockUser.Id).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botDisableCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "could not disable bot 'a-bot'")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestBotEnableCmd() {
|
||||
s.Run("Should enable a bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
|
||||
mockBot := model.Bot{Username: botArg, DisplayName: "some-name", Description: "some-text"}
|
||||
mockUser := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
EnableBot(context.TODO(), mockUser.Id).
|
||||
Return(&mockBot, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botEnableCmdF(s.client, &cobra.Command{}, []string{botArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should error when user not found bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), botArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botEnableCmdF(s.client, &cobra.Command{}, []string{botArg})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "can't find user 'a-bot'")
|
||||
})
|
||||
|
||||
s.Run("Should error when enabling bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("display-name", "some-name", "")
|
||||
cmd.Flags().String("description", "some-text", "")
|
||||
cmd.Flags().Lookup("display-name").Changed = true
|
||||
cmd.Flags().Lookup("description").Changed = true
|
||||
mockUser := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
EnableBot(context.TODO(), mockUser.Id).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botEnableCmdF(s.client, cmd, []string{botArg})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "could not enable bot 'a-bot'")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestBotAssignCmd() {
|
||||
s.Run("Should assign a bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
userArg := "a-user"
|
||||
|
||||
mockBot := model.Bot{Username: botArg, DisplayName: "some-name", Description: "some-text"}
|
||||
mockBotUser := model.User{Id: model.NewId()}
|
||||
mockNewOwner := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockBotUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), userArg, "").
|
||||
Return(&mockNewOwner, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
AssignBot(context.TODO(), mockBotUser.Id, mockNewOwner.Id).
|
||||
Return(&mockBot, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := botAssignCmdF(s.client, &cobra.Command{}, []string{botArg, userArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(&mockBot, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Should error when bot user not found", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
userArg := "a-user"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), botArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botAssignCmdF(s.client, &cobra.Command{}, []string{botArg, userArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "unable to find user 'a-bot'")
|
||||
})
|
||||
|
||||
s.Run("Should error when new owner not found", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
userArg := "a-user"
|
||||
|
||||
mockBotUser := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockBotUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), userArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), userArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botAssignCmdF(s.client, &cobra.Command{}, []string{botArg, userArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "unable to find user 'a-user'")
|
||||
})
|
||||
|
||||
s.Run("Should error when assigning bot", func() {
|
||||
printer.Clean()
|
||||
|
||||
botArg := "a-bot"
|
||||
userArg := "a-user"
|
||||
|
||||
mockBotUser := model.User{Id: model.NewId()}
|
||||
mockNewOwner := model.User{Id: model.NewId()}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), botArg, "").
|
||||
Return(&mockBotUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), userArg, "").
|
||||
Return(&mockNewOwner, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
AssignBot(context.TODO(), mockBotUser.Id, mockNewOwner.Id).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := botAssignCmdF(s.client, &cobra.Command{}, []string{botArg, userArg})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Contains(err.Error(), "can not assign bot 'a-bot' to user 'a-user'")
|
||||
})
|
||||
}
|
||||
709
server/cmd/mmctl/commands/channel.go
Normal file
709
server/cmd/mmctl/commands/channel.go
Normal file
@@ -0,0 +1,709 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var ChannelCmd = &cobra.Command{
|
||||
Use: "channel",
|
||||
Short: "Management of channels",
|
||||
}
|
||||
|
||||
var ChannelCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a channel",
|
||||
Long: `Create a channel.`,
|
||||
Example: ` channel create --team myteam --name mynewchannel --display-name "My New Channel"
|
||||
channel create --team myteam --name mynewprivatechannel --display-name "My New Private Channel" --private`,
|
||||
RunE: withClient(createChannelCmdF),
|
||||
}
|
||||
|
||||
// ChannelRenameCmd is used to change name and/or display name of an existing channel.
|
||||
var ChannelRenameCmd = &cobra.Command{
|
||||
Use: "rename [channel]",
|
||||
Short: "Rename channel",
|
||||
Long: `Rename an existing channel.`,
|
||||
Example: ` channel rename myteam:oldchannel --name 'new-channel' --display-name 'New Display Name'
|
||||
channel rename myteam:oldchannel --name 'new-channel'
|
||||
channel rename myteam:oldchannel --display-name 'New Display Name'`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(renameChannelCmdF),
|
||||
}
|
||||
|
||||
var ArchiveChannelsCmd = &cobra.Command{
|
||||
Use: "archive [channels]",
|
||||
Short: "Archive channels",
|
||||
Long: `Archive some channels.
|
||||
Archive a channel along with all related information including posts from the database.
|
||||
Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`,
|
||||
Example: " channel archive myteam:mychannel",
|
||||
RunE: withClient(archiveChannelsCmdF),
|
||||
}
|
||||
|
||||
var DeleteChannelsCmd = &cobra.Command{
|
||||
Use: "delete [channels]",
|
||||
Short: "Delete channels",
|
||||
Long: `Permanently delete some channels.
|
||||
Permanently deletes one or multiple channels along with all related information including posts from the database.`,
|
||||
Example: " channel delete myteam:mychannel",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: withClient(deleteChannelsCmdF),
|
||||
}
|
||||
|
||||
// ListChannelsCmd is a command which lists all the channels of team(s) in a server.
|
||||
var ListChannelsCmd = &cobra.Command{
|
||||
Use: "list [teams]",
|
||||
Short: "List all channels on specified teams.",
|
||||
Long: `List all channels on specified teams.
|
||||
Archived channels are appended with ' (archived)'.
|
||||
Private channels the user is a member of or has access to are appended with ' (private)'.`,
|
||||
Example: " channel list myteam",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: withClient(listChannelsCmdF),
|
||||
}
|
||||
|
||||
var ModifyChannelCmd = &cobra.Command{
|
||||
Use: "modify [channel] [flags]",
|
||||
Short: "Modify a channel's public/private type",
|
||||
Long: `Change the Public/Private type of a channel.
|
||||
Channel can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`,
|
||||
Example: ` channel modify myteam:mychannel --private
|
||||
channel modify channelId --public`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(modifyChannelCmdF),
|
||||
}
|
||||
|
||||
var UnarchiveChannelCmd = &cobra.Command{
|
||||
Use: "unarchive [channels]",
|
||||
Short: "Unarchive some channels",
|
||||
Long: `Unarchive a previously archived channel
|
||||
Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`,
|
||||
Example: " channel unarchive myteam:mychannel",
|
||||
RunE: withClient(unarchiveChannelsCmdF),
|
||||
}
|
||||
|
||||
var SearchChannelCmd = &cobra.Command{
|
||||
Use: "search [channel]\n mmctl search --team [team] [channel]",
|
||||
Short: "Search a channel",
|
||||
Long: `Search a channel by channel name.
|
||||
Channel can be specified by team. ie. --team myteam mychannel or by team ID.`,
|
||||
Example: ` channel search mychannel
|
||||
channel search --team myteam mychannel`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(searchChannelCmdF),
|
||||
}
|
||||
|
||||
var MoveChannelCmd = &cobra.Command{
|
||||
Use: "move [team] [channels]",
|
||||
Short: "Moves channels to the specified team",
|
||||
Long: `Moves the provided channels to the specified team.
|
||||
Validates that all users in the channel belong to the target team. If some users are not members of the target team, the move fails and the missing users are listed; use --auto-add-users to add them to the target team automatically, or --force to remove them from the channel. Incoming/Outgoing webhooks are moved along with the channel.
|
||||
Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`,
|
||||
Example: " channel move newteam oldteam:mychannel",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: withClient(moveChannelCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ChannelCreateCmd.Flags().String("name", "", "Channel Name")
|
||||
ChannelCreateCmd.Flags().String("display-name", "", "Channel Display Name")
|
||||
ChannelCreateCmd.Flags().String("team", "", "Team name or ID")
|
||||
ChannelCreateCmd.Flags().String("header", "", "Channel header")
|
||||
ChannelCreateCmd.Flags().String("purpose", "", "Channel purpose")
|
||||
ChannelCreateCmd.Flags().Bool("private", false, "Create a private channel.")
|
||||
|
||||
ModifyChannelCmd.Flags().Bool("private", false, "Convert the channel to a private channel")
|
||||
ModifyChannelCmd.Flags().Bool("public", false, "Convert the channel to a public channel")
|
||||
|
||||
ChannelRenameCmd.Flags().String("name", "", "Channel Name")
|
||||
ChannelRenameCmd.Flags().String("display-name", "", "Channel Display Name")
|
||||
|
||||
ListChannelsCmd.Flags().BoolP("show-ids", "i", false, "Show channel IDs")
|
||||
|
||||
SearchChannelCmd.Flags().String("team", "", "Team name or ID")
|
||||
|
||||
MoveChannelCmd.Flags().Bool("force", false, "Remove users that are not members of target team before moving the channel.")
|
||||
MoveChannelCmd.Flags().Bool("auto-add-users", false, "Add users that are not members of the target team to it before moving the channel.")
|
||||
|
||||
DeleteChannelsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the channel and a DB backup has been performed.")
|
||||
|
||||
ChannelCmd.AddCommand(
|
||||
ChannelCreateCmd,
|
||||
ArchiveChannelsCmd,
|
||||
ListChannelsCmd,
|
||||
UnarchiveChannelCmd,
|
||||
ModifyChannelCmd,
|
||||
ChannelRenameCmd,
|
||||
SearchChannelCmd,
|
||||
MoveChannelCmd,
|
||||
DeleteChannelsCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(ChannelCmd)
|
||||
}
|
||||
|
||||
func createChannelCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
name, errn := cmd.Flags().GetString("name")
|
||||
if errn != nil || name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
displayname, errdn := cmd.Flags().GetString("display-name")
|
||||
if errdn != nil || displayname == "" {
|
||||
return errors.New("display-name is required")
|
||||
}
|
||||
teamArg, errteam := cmd.Flags().GetString("team")
|
||||
if errteam != nil || teamArg == "" {
|
||||
return errors.New("team is required")
|
||||
}
|
||||
header, _ := cmd.Flags().GetString("header")
|
||||
purpose, _ := cmd.Flags().GetString("purpose")
|
||||
useprivate, _ := cmd.Flags().GetBool("private")
|
||||
|
||||
channelType := model.ChannelTypeOpen
|
||||
if useprivate {
|
||||
channelType = model.ChannelTypePrivate
|
||||
}
|
||||
|
||||
team := getTeamFromTeamArg(c, teamArg)
|
||||
if team == nil {
|
||||
return errors.Errorf("unable to find team: %s", teamArg)
|
||||
}
|
||||
|
||||
channel := &model.Channel{
|
||||
TeamId: team.Id,
|
||||
Name: name,
|
||||
DisplayName: displayname,
|
||||
Header: header,
|
||||
Purpose: purpose,
|
||||
Type: channelType,
|
||||
CreatorId: "",
|
||||
}
|
||||
|
||||
newChannel, _, err := c.CreateChannel(context.TODO(), channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.PrintT("New channel {{.Name}} successfully created", newChannel)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func archiveChannelsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("enter at least one channel to archive")
|
||||
}
|
||||
|
||||
channels := getChannelsFromChannelArgs(c, args)
|
||||
var errors *multierror.Error
|
||||
for i, channel := range channels {
|
||||
if channel == nil {
|
||||
printer.PrintError("Unable to find channel '" + args[i] + "'")
|
||||
errors = multierror.Append(errors, fmt.Errorf("unable to find channel %q", args[i]))
|
||||
continue
|
||||
}
|
||||
if _, err := c.DeleteChannel(context.TODO(), channel.Id); err != nil {
|
||||
printer.PrintError("Unable to archive channel '" + channel.Name + "' error: " + err.Error())
|
||||
errors = multierror.Append(errors, fmt.Errorf("unable to archive channel %q, error: %w", channel.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.ErrorOrNil()
|
||||
}
|
||||
|
||||
func getAllPublicChannelsForTeam(c client.Client, teamID string) ([]*model.Channel, error) {
|
||||
channels := []*model.Channel{}
|
||||
page := 0
|
||||
|
||||
for {
|
||||
channelsPage, _, err := c.GetPublicChannelsForTeam(context.TODO(), teamID, page, DefaultPageSize, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(channelsPage) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
channels = append(channels, channelsPage...)
|
||||
page++
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func getAllDeletedChannelsForTeam(c client.Client, teamID string) ([]*model.Channel, error) {
|
||||
channels := []*model.Channel{}
|
||||
page := 0
|
||||
|
||||
for {
|
||||
channelsPage, _, err := c.GetDeletedChannelsForTeam(context.TODO(), teamID, page, DefaultPageSize, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(channelsPage) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
channels = append(channels, channelsPage...)
|
||||
page++
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func listChannelsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
teams := getTeamsFromTeamArgs(c, args)
|
||||
|
||||
showIds, _ := cmd.Flags().GetBool("show-ids")
|
||||
namePrefix := "{{.Name}}"
|
||||
if showIds {
|
||||
namePrefix = "{{.Id}}: {{.Name}}"
|
||||
}
|
||||
|
||||
var multierr *multierror.Error
|
||||
for i, team := range teams {
|
||||
if team == nil {
|
||||
err := fmt.Errorf("unable to find team %q", args[i])
|
||||
printer.PrintError(err.Error())
|
||||
multierr = multierror.Append(multierr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
publicChannels, err := getAllPublicChannelsForTeam(c, team.Id)
|
||||
if err != nil {
|
||||
printer.PrintError(fmt.Sprintf("unable to list public channels for %q: %s", args[i], err))
|
||||
multierr = multierror.Append(multierr, err)
|
||||
}
|
||||
for _, channel := range publicChannels {
|
||||
printer.PrintT(namePrefix, channel)
|
||||
}
|
||||
|
||||
deletedChannels, err := getAllDeletedChannelsForTeam(c, team.Id)
|
||||
if err != nil {
|
||||
printer.PrintError(fmt.Sprintf("unable to list archived channels for %q: %s", args[i], err))
|
||||
multierr = multierror.Append(multierr, err)
|
||||
}
|
||||
for _, channel := range deletedChannels {
|
||||
printer.PrintT(namePrefix+" (archived)", channel)
|
||||
}
|
||||
|
||||
privateChannels, appErr := getPrivateChannels(c, team.Id)
|
||||
if appErr != nil {
|
||||
printer.PrintError(fmt.Sprintf("unable to list private channels for %q: %s", args[i], appErr.Error()))
|
||||
multierr = multierror.Append(multierr, appErr)
|
||||
}
|
||||
for _, channel := range privateChannels {
|
||||
printer.PrintT(namePrefix+" (private)", channel)
|
||||
}
|
||||
}
|
||||
|
||||
return multierr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func unarchiveChannelsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("enter at least one channel")
|
||||
}
|
||||
|
||||
var errs *multierror.Error
|
||||
|
||||
channels := getChannelsFromChannelArgs(c, args)
|
||||
for i, channel := range channels {
|
||||
if channel == nil {
|
||||
msg := "Unable to find channel '" + args[i] + "'"
|
||||
printer.PrintError(msg)
|
||||
errs = multierror.Append(errs, errors.New(msg))
|
||||
continue
|
||||
}
|
||||
if _, _, err := c.RestoreChannel(context.TODO(), channel.Id); err != nil {
|
||||
msg := "Unable to unarchive channel '" + args[i] + "'. Error: " + err.Error()
|
||||
printer.PrintError(msg)
|
||||
errs = multierror.Append(errs, errors.New(msg))
|
||||
}
|
||||
}
|
||||
|
||||
return errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
func modifyChannelCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
public, _ := cmd.Flags().GetBool("public")
|
||||
private, _ := cmd.Flags().GetBool("private")
|
||||
|
||||
if public == private {
|
||||
return errors.New("you must specify only one of --public or --private")
|
||||
}
|
||||
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.Errorf("unable to find channel %q", args[0])
|
||||
}
|
||||
|
||||
if !(channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypePrivate) {
|
||||
return errors.New("you can only change the type of public/private channels")
|
||||
}
|
||||
|
||||
privacy := model.ChannelTypeOpen
|
||||
if private {
|
||||
privacy = model.ChannelTypePrivate
|
||||
}
|
||||
|
||||
if _, _, err := c.UpdateChannelPrivacy(context.TODO(), channel.Id, privacy); err != nil {
|
||||
return errors.Errorf("failed to update channel (%q) privacy: %s", args[0], err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func renameChannelCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
existingTeamChannel := args[0]
|
||||
|
||||
newChannelName, err := cmd.Flags().GetString("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newDisplayName, err := cmd.Flags().GetString("display-name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// At least one of display name or name flag must be present
|
||||
if newDisplayName == "" && newChannelName == "" {
|
||||
return errors.New("require at least one flag to rename channel, either 'name' or 'display-name'")
|
||||
}
|
||||
|
||||
channel := getChannelFromChannelArg(c, existingTeamChannel)
|
||||
if channel == nil {
|
||||
return errors.Errorf("unable to find channel from %q", existingTeamChannel)
|
||||
}
|
||||
|
||||
channelPatch := &model.ChannelPatch{}
|
||||
if newChannelName != "" {
|
||||
channelPatch.Name = &newChannelName
|
||||
}
|
||||
if newDisplayName != "" {
|
||||
channelPatch.DisplayName = &newDisplayName
|
||||
}
|
||||
|
||||
// Using PatchChannel API to rename channel
|
||||
updatedChannel, _, err := c.PatchChannel(context.TODO(), channel.Id, channelPatch)
|
||||
if err != nil {
|
||||
return errors.Errorf("cannot rename channel %q, error: %s", channel.Name, err.Error())
|
||||
}
|
||||
|
||||
printer.PrintT("'{{.Name}}' channel renamed", updatedChannel)
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchChannelCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
var channel *model.Channel
|
||||
|
||||
if teamArg, _ := cmd.Flags().GetString("team"); teamArg != "" {
|
||||
team := getTeamFromTeamArg(c, teamArg)
|
||||
if team == nil {
|
||||
return errors.Errorf("team %s was not found", teamArg)
|
||||
}
|
||||
|
||||
var err error
|
||||
channel, _, err = c.GetChannelByName(context.TODO(), args[0], team.Id, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if channel == nil {
|
||||
return errors.Errorf("channel %s was not found in team %s", args[0], teamArg)
|
||||
}
|
||||
} else {
|
||||
teams, err := getPages(func(page, numPerPage int, etag string) ([]*model.Team, *model.Response, error) {
|
||||
return c.GetAllTeams(context.TODO(), etag, page, numPerPage)
|
||||
}, DefaultPageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
channel, _, _ = c.GetChannelByName(context.TODO(), args[0], team.Id, "")
|
||||
if channel != nil && channel.Name == args[0] {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if channel == nil {
|
||||
return errors.Errorf("channel %q was not found in any team", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
if channel.DeleteAt > 0 {
|
||||
printer.PrintT("Channel Name :{{.Name}}, Display Name :{{.DisplayName}}, Channel ID :{{.Id}} (archived)", channel)
|
||||
} else {
|
||||
printer.PrintT("Channel Name :{{.Name}}, Display Name :{{.DisplayName}}, Channel ID :{{.Id}}", channel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveChannelCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
autoAddUsers, _ := cmd.Flags().GetBool("auto-add-users")
|
||||
|
||||
team := getTeamFromTeamArg(c, args[0])
|
||||
if team == nil {
|
||||
return fmt.Errorf("unable to find destination team %q", args[0])
|
||||
}
|
||||
|
||||
var result *multierror.Error
|
||||
|
||||
channels := getChannelsFromChannelArgs(c, args[1:])
|
||||
for i, channel := range channels {
|
||||
if channel == nil {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to find channel %q", args[i+1]))
|
||||
continue
|
||||
}
|
||||
|
||||
if channel.TeamId == team.Id {
|
||||
continue
|
||||
}
|
||||
|
||||
// When users are not being forcibly removed, the server rejects the move
|
||||
// if any channel member is missing from the destination team. Surface
|
||||
// those users to the operator, adding them to the team first if requested.
|
||||
if !force {
|
||||
missingUsers, err := getChannelMembersNotInTeam(c, channel.Id, team.Id)
|
||||
if err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to determine missing team members for channel %q: %w", channel.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if len(missingUsers) > 0 {
|
||||
if !autoAddUsers {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to move channel %q: the following users are not members of team %q: %s. Re-run with --auto-add-users to add them automatically, or with --force to remove them from the channel", channel.Name, team.Name, strings.Join(usernamesOf(missingUsers), ", ")))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := addUsersToTeam(c, team, missingUsers); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newChannel, _, err := c.MoveChannel(context.TODO(), channel.Id, team.Id, force)
|
||||
if err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to move channel %q: %w", channel.Name, err))
|
||||
continue
|
||||
}
|
||||
printer.PrintT(fmt.Sprintf("Moved channel {{.Name}} to %q ({{.TeamId}}) from %s.", team.Name, channel.TeamId), newChannel)
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
// getChannelMembersNotInTeam returns the users that are members of the channel
|
||||
// but are not members of the given team.
|
||||
func getChannelMembersNotInTeam(c client.Client, channelID, teamID string) ([]*model.User, error) {
|
||||
channelMemberUserIDs, err := getAllChannelMemberUserIDs(c, channelID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get channel members: %w", err)
|
||||
}
|
||||
|
||||
if len(channelMemberUserIDs) == 0 {
|
||||
return []*model.User{}, nil
|
||||
}
|
||||
|
||||
teamMembers, err := getTeamMembersByUserIDs(c, teamID, channelMemberUserIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get team members: %w", err)
|
||||
}
|
||||
|
||||
teamMemberUserIDs := make(map[string]bool, len(teamMembers))
|
||||
for _, member := range teamMembers {
|
||||
teamMemberUserIDs[member.UserId] = true
|
||||
}
|
||||
|
||||
missingUserIDs := []string{}
|
||||
for _, userID := range channelMemberUserIDs {
|
||||
if !teamMemberUserIDs[userID] {
|
||||
missingUserIDs = append(missingUserIDs, userID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingUserIDs) == 0 {
|
||||
return []*model.User{}, nil
|
||||
}
|
||||
|
||||
users, _, err := c.GetUsersByIds(context.TODO(), missingUserIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get users for missing channel members: %w", err)
|
||||
}
|
||||
|
||||
usersByID := make(map[string]*model.User, len(users))
|
||||
for _, user := range users {
|
||||
usersByID[user.Id] = user
|
||||
}
|
||||
|
||||
missingUsers := make([]*model.User, 0, len(missingUserIDs))
|
||||
for _, userID := range missingUserIDs {
|
||||
if user, ok := usersByID[userID]; ok {
|
||||
missingUsers = append(missingUsers, user)
|
||||
continue
|
||||
}
|
||||
missingUsers = append(missingUsers, &model.User{Id: userID, Username: userID})
|
||||
}
|
||||
|
||||
return missingUsers, nil
|
||||
}
|
||||
|
||||
func getAllChannelMemberUserIDs(c client.Client, channelID string) ([]string, error) {
|
||||
userIDs := []string{}
|
||||
page := 0
|
||||
|
||||
for {
|
||||
membersPage, _, err := c.GetChannelMembers(context.TODO(), channelID, page, DefaultPageSize, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(membersPage) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, member := range membersPage {
|
||||
userIDs = append(userIDs, member.UserId)
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
func getTeamMembersByUserIDs(c client.Client, teamID string, userIDs []string) ([]*model.TeamMember, error) {
|
||||
teamMembers := []*model.TeamMember{}
|
||||
|
||||
for i := 0; i < len(userIDs); i += DefaultPageSize {
|
||||
end := min(i+DefaultPageSize, len(userIDs))
|
||||
|
||||
members, _, err := c.GetTeamMembersByIds(context.TODO(), teamID, userIDs[i:end])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teamMembers = append(teamMembers, members...)
|
||||
}
|
||||
|
||||
return teamMembers, nil
|
||||
}
|
||||
|
||||
func addUsersToTeam(c client.Client, team *model.Team, users []*model.User) error {
|
||||
var result *multierror.Error
|
||||
for _, user := range users {
|
||||
if _, _, err := c.AddTeamMember(context.TODO(), team.Id, user.Id); err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to add user %q to team %q: %w", user.Username, team.Name, err))
|
||||
continue
|
||||
}
|
||||
printer.PrintT(fmt.Sprintf("Added user {{.Username}} to team %q.", team.Name), user)
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func usernamesOf(users []*model.User) []string {
|
||||
names := make([]string, len(users))
|
||||
for i, user := range users {
|
||||
names[i] = user.Username
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func getPrivateChannels(c client.Client, teamID string) ([]*model.Channel, error) {
|
||||
allPrivateChannels := []*model.Channel{}
|
||||
page := 0
|
||||
withoutError := true
|
||||
|
||||
for {
|
||||
channelsPage, _, err := c.GetPrivateChannelsForTeam(context.TODO(), teamID, page, DefaultPageSize, "")
|
||||
if err != nil && viper.GetBool("local") {
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
// This means that the user is not in local mode neither
|
||||
// an admin, so we need to continue fetching the private
|
||||
// channels specific to their credentials
|
||||
withoutError = false
|
||||
break
|
||||
}
|
||||
|
||||
if len(channelsPage) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
allPrivateChannels = append(allPrivateChannels, channelsPage...)
|
||||
page++
|
||||
}
|
||||
|
||||
// if the break happened without an error, this means we're either
|
||||
// in local mode or an admin, and we'll have all private channels
|
||||
// by now, so we can safely return
|
||||
if withoutError {
|
||||
return allPrivateChannels, nil
|
||||
}
|
||||
|
||||
// We are definitely not in local mode here so we can safely use
|
||||
// "GetChannelsForTeamForUser" and "me" for userId
|
||||
allChannels, response, err := c.GetChannelsForTeamForUser(context.TODO(), teamID, "me", false, "")
|
||||
if err != nil {
|
||||
if response.StatusCode == http.StatusNotFound { // user doesn't belong to any channels
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
privateChannels := make([]*model.Channel, 0, len(allChannels))
|
||||
for _, channel := range allChannels {
|
||||
if channel.Type != model.ChannelTypePrivate {
|
||||
continue
|
||||
}
|
||||
privateChannels = append(privateChannels, channel)
|
||||
}
|
||||
return privateChannels, nil
|
||||
}
|
||||
|
||||
func deleteChannelsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
confirmFlag, _ := cmd.Flags().GetBool("confirm")
|
||||
if !confirmFlag {
|
||||
if err := getConfirmation("Are you sure you want to delete the channels specified? All data will be permanently deleted?", true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var result *multierror.Error
|
||||
|
||||
channels := getChannelsFromChannelArgs(c, args)
|
||||
for i, channel := range channels {
|
||||
if channel == nil {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to find channel '%s'", args[i]))
|
||||
continue
|
||||
}
|
||||
if _, err := c.PermanentDeleteChannel(context.TODO(), channel.Id); err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to delete channel '%q' error: %w", channel.Name, err))
|
||||
} else {
|
||||
printer.PrintT("Deleted channel '{{.Name}}'", channel)
|
||||
}
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
730
server/cmd/mmctl/commands/channel_e2e_test.go
Normal file
730
server/cmd/mmctl/commands/channel_e2e_test.go
Normal file
@@ -0,0 +1,730 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestListChannelsCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
var assertChannelNames = func(want []string, lines []any) {
|
||||
var got []string
|
||||
for i := range lines {
|
||||
got = append(got, lines[i].(*model.Channel).Name)
|
||||
}
|
||||
|
||||
s.ElementsMatch(want, got)
|
||||
}
|
||||
|
||||
s.Run("List channels/Client", func() {
|
||||
printer.Clean()
|
||||
wantNames := append(
|
||||
s.th.App.DefaultChannelNames(s.th.Context),
|
||||
[]string{
|
||||
s.th.BasicChannel.Name,
|
||||
s.th.BasicChannel2.Name,
|
||||
s.th.BasicDeletedChannel.Name,
|
||||
s.th.BasicPrivateChannel.Name,
|
||||
}...,
|
||||
)
|
||||
|
||||
err := listChannelsCmdF(s.th.Client, &cobra.Command{}, []string{s.th.BasicTeam.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Equal(6, len(printer.GetLines()))
|
||||
assertChannelNames(wantNames, printer.GetLines())
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("List channels", func(c client.Client) {
|
||||
printer.Clean()
|
||||
wantNames := append(
|
||||
s.th.App.DefaultChannelNames(s.th.Context),
|
||||
[]string{
|
||||
s.th.BasicChannel.Name,
|
||||
s.th.BasicChannel2.Name,
|
||||
s.th.BasicDeletedChannel.Name,
|
||||
s.th.BasicPrivateChannel.Name,
|
||||
s.th.BasicPrivateChannel2.Name,
|
||||
}...,
|
||||
)
|
||||
|
||||
err := listChannelsCmdF(c, &cobra.Command{}, []string{s.th.BasicTeam.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Equal(7, len(printer.GetLines()))
|
||||
assertChannelNames(wantNames, printer.GetLines())
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForAllClients("List channels for non existent team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
team := "non-existent-team"
|
||||
|
||||
err := listChannelsCmdF(c, &cobra.Command{}, []string{team})
|
||||
s.Require().ErrorContains(err, "unable to find team \""+team+"\"")
|
||||
s.Len(printer.GetErrorLines(), 1)
|
||||
s.Equal("unable to find team \""+team+"\"", printer.GetErrorLines()[0])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestSearchChannelCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForAllClients("Search nonexistent channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := searchChannelCmdF(c, &cobra.Command{}, []string{"test"})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(`channel "test" was not found in any team`, err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Search existing channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := searchChannelCmdF(c, &cobra.Command{}, []string{s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
actualChannel, ok := printer.GetLines()[0].(*model.Channel)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(s.th.BasicChannel.Name, actualChannel.Name)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Search existing channel of a team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("team", s.th.BasicChannel.TeamId, "")
|
||||
|
||||
err := searchChannelCmdF(c, cmd, []string{s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
actualChannel, ok := printer.GetLines()[0].(*model.Channel)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(s.th.BasicChannel.Name, actualChannel.Name)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Search existing channel that does not belong to a team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
testTeamName := api4.GenerateTestTeamName()
|
||||
|
||||
team, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
Name: testTeamName,
|
||||
DisplayName: "dn_" + testTeamName,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("team", team.Id, "")
|
||||
|
||||
err := searchChannelCmdF(c, cmd, []string{s.th.BasicChannel.Name})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, `Channel does not exist.`)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Search existing channel should fail for Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := searchChannelCmdF(s.th.Client, &cobra.Command{}, []string{s.th.BasicChannel.Name})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("channel \"%s\" was not found in any team", s.th.BasicChannel.Name), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestCreateChannelCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForAllClients("create channel successfully", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
channelName := model.NewRandomString(10)
|
||||
teamName := s.th.BasicTeam.Name
|
||||
channelDisplayName := "channelDisplayName"
|
||||
cmd.Flags().String("name", channelName, "channel name")
|
||||
cmd.Flags().String("team", teamName, "team name")
|
||||
cmd.Flags().String("display-name", channelDisplayName, "display name")
|
||||
|
||||
err := createChannelCmdF(c, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
|
||||
printerChannel := printer.GetLines()[0].(*model.Channel)
|
||||
s.Require().Equal(channelName, printerChannel.Name)
|
||||
s.Require().Equal(s.th.BasicTeam.Id, printerChannel.TeamId)
|
||||
|
||||
newChannel, err := s.th.App.GetChannelByName(s.th.Context, channelName, s.th.BasicTeam.Id, false)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(channelName, newChannel.Name)
|
||||
s.Require().Equal(channelDisplayName, newChannel.DisplayName)
|
||||
s.Require().Equal(s.th.BasicTeam.Id, newChannel.TeamId)
|
||||
})
|
||||
|
||||
s.RunForAllClients("create channel with nonexistent team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
channelName := model.NewRandomString(10)
|
||||
teamName := "nonexistent team"
|
||||
channelDisplayName := "channelDisplayName"
|
||||
cmd.Flags().String("name", channelName, "channel name")
|
||||
cmd.Flags().String("team", teamName, "team name")
|
||||
cmd.Flags().String("display-name", channelDisplayName, "display name")
|
||||
|
||||
err := createChannelCmdF(c, cmd, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("unable to find team: "+teamName, err.Error())
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
|
||||
_, err = s.th.App.GetChannelByName(s.th.Context, channelName, s.th.BasicTeam.Id, false)
|
||||
s.Require().NotNil(err)
|
||||
})
|
||||
|
||||
s.RunForAllClients("create channel with invalid name", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
channelName := "invalid name"
|
||||
teamName := s.th.BasicTeam.Name
|
||||
channelDisplayName := "channelDisplayName"
|
||||
cmd.Flags().String("name", channelName, "channel name")
|
||||
cmd.Flags().String("team", teamName, "team name")
|
||||
cmd.Flags().String("display-name", channelDisplayName, "display name")
|
||||
|
||||
err := createChannelCmdF(c, cmd, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "Name must be 1 or more lowercase alphanumeric character")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
|
||||
_, err = s.th.App.GetChannelByName(s.th.Context, channelName, s.th.BasicTeam.Id, false)
|
||||
s.Require().NotNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestArchiveChannelsCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("Archive channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := archiveChannelsCmdF(s.th.SystemAdminClient, &cobra.Command{}, []string{fmt.Sprintf("%s:%s", s.th.BasicTeam.Id, s.th.BasicChannel.Name)})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Archive channel without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := archiveChannelsCmdF(s.th.LocalClient, &cobra.Command{}, []string{fmt.Sprintf("%s:%s", s.th.BasicTeam.Id, s.th.BasicChannel.Name)})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("Unable to archive channel '%s'", s.th.BasicChannel.Name))
|
||||
})
|
||||
|
||||
s.RunForAllClients("Archive nonexistent channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := archiveChannelsCmdF(c, &cobra.Command{}, []string{fmt.Sprintf("%s:%s", s.th.BasicTeam.Id, "nonexistent-channel")})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("Unable to find channel '%s:%s'", s.th.BasicTeam.Id, "nonexistent-channel"))
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Archive deleted channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := archiveChannelsCmdF(c, &cobra.Command{}, []string{fmt.Sprintf("%s:%s", s.th.BasicTeam.Id, s.th.BasicDeletedChannel.Name)})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("Unable to archive channel '%s'", s.th.BasicDeletedChannel.Name))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "The channel has been archived or deleted.")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestUnarchiveChannelsCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("Unarchive channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := unarchiveChannelsCmdF(s.th.SystemAdminClient, &cobra.Command{}, []string{fmt.Sprintf("%s:%s", s.th.BasicTeam.Id, s.th.BasicDeletedChannel.Name)})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
channel, appErr := s.th.App.GetChannel(s.th.Context, s.th.BasicDeletedChannel.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().True(channel.IsOpen())
|
||||
})
|
||||
|
||||
s.Run("Unarchive channel without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := unarchiveChannelsCmdF(s.th.Client, &cobra.Command{}, []string{fmt.Sprintf("%s:%s", s.th.BasicTeam.Id, s.th.BasicDeletedChannel.Name)})
|
||||
expectedError := fmt.Sprintf("Unable to unarchive channel '%s:%s'", s.th.BasicTeam.Id, s.th.BasicDeletedChannel.Name)
|
||||
s.Require().ErrorContains(err, expectedError)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], expectedError)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "You do not have the appropriate permissions.")
|
||||
})
|
||||
|
||||
s.RunForAllClients("Unarchive nonexistent channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := unarchiveChannelsCmdF(c, &cobra.Command{}, []string{fmt.Sprintf("%s:%s", s.th.BasicTeam.Id, "nonexistent-channel")})
|
||||
expectedError := fmt.Sprintf("Unable to find channel '%s:%s'", s.th.BasicTeam.Id, "nonexistent-channel")
|
||||
s.Require().ErrorContains(err, expectedError)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], expectedError)
|
||||
})
|
||||
|
||||
s.Run("Unarchive open channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := unarchiveChannelsCmdF(s.th.SystemAdminClient, &cobra.Command{}, []string{fmt.Sprintf("%s:%s", s.th.BasicTeam.Id, s.th.BasicChannel.Name)})
|
||||
expectedError := fmt.Sprintf("Unable to unarchive channel '%s:%s'", s.th.BasicTeam.Id, s.th.BasicChannel.Name)
|
||||
s.Require().ErrorContains(err, expectedError)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], expectedError)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "Unable to unarchive channel. The channel is not archived.")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestDeleteChannelsCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
previousConfig := s.th.App.Config().ServiceSettings.EnableAPIChannelDeletion
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIChannelDeletion = true })
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIChannelDeletion = *previousConfig })
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
team, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
DisplayName: "Best Team",
|
||||
Name: "best-team",
|
||||
Type: model.TeamOpen,
|
||||
Email: s.th.GenerateTestEmail(),
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
otherChannel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{Type: model.ChannelTypeOpen, Name: "channel_you_are_not_authorized_to", CreatorId: user.Id}, true)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForSystemAdminAndLocal("Delete channel", func(c client.Client) {
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{Type: model.ChannelTypeOpen, Name: "channel_name", CreatorId: user.Id}, true)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
args := []string{team.Id + ":" + channel.Id}
|
||||
|
||||
printer.Clean()
|
||||
err := deleteChannelsCmdF(c, cmd, args)
|
||||
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(channel, printer.GetLines()[0])
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
_, err = s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
|
||||
s.Require().NotNil(err)
|
||||
s.CheckErrorID(err, "app.channel.get.existing.app_error")
|
||||
})
|
||||
|
||||
s.Run("Delete channel with disabled config as system admin", func() {
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{Type: model.ChannelTypeOpen, Name: "channel_name_you_cannot_delete", CreatorId: user.Id}, true)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
previousVal := s.th.App.Config().ServiceSettings.EnableAPIChannelDeletion
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIChannelDeletion = false })
|
||||
defer func() {
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIChannelDeletion = *previousVal })
|
||||
}()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
args := []string{team.Id + ":" + channel.Id}
|
||||
|
||||
printer.Clean()
|
||||
err := deleteChannelsCmdF(s.th.SystemAdminClient, cmd, args)
|
||||
|
||||
var expected error
|
||||
expected = multierror.Append(expected, errors.New("unable to delete channel '\""+channel.Name+"\"' error: Permanent channel deletion feature is not enabled. ServiceSettings.EnableAPIChannelDeletion must be set to true to use this command. See https://mattermost.com/pl/environment-configuration-settings for more information"))
|
||||
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, expected.Error())
|
||||
|
||||
channel, err = s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
|
||||
s.Require().Nil(err)
|
||||
s.Require().NotNil(channel)
|
||||
})
|
||||
|
||||
s.Run("Delete channel with disabled config as local client", func() {
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{Type: model.ChannelTypeOpen, Name: "channel_name", CreatorId: user.Id}, true)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
previousVal := s.th.App.Config().ServiceSettings.EnableAPIChannelDeletion
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIChannelDeletion = false })
|
||||
defer func() {
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIChannelDeletion = *previousVal })
|
||||
}()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
args := []string{team.Id + ":" + channel.Id}
|
||||
|
||||
printer.Clean()
|
||||
err := deleteChannelsCmdF(s.th.LocalClient, cmd, args)
|
||||
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(channel, printer.GetLines()[0])
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// expect the channel deleted
|
||||
_, err = s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
s.Require().NotNil(err)
|
||||
s.CheckErrorID(err, "app.channel.get.existing.app_error")
|
||||
})
|
||||
|
||||
s.Run("Delete channel without permissions", func() {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
args := []string{team.Id + ":" + otherChannel.Id}
|
||||
|
||||
printer.Clean()
|
||||
err := deleteChannelsCmdF(s.th.Client, cmd, args)
|
||||
|
||||
arg := team.Id + ":" + otherChannel.Id
|
||||
var expected error
|
||||
expected = multierror.Append(expected, errors.New("unable to find channel '"+arg+"'"))
|
||||
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, expected.Error())
|
||||
|
||||
channel, err := s.th.App.GetChannel(s.th.Context, otherChannel.Id)
|
||||
|
||||
s.Require().Nil(err)
|
||||
s.Require().NotNil(channel)
|
||||
})
|
||||
|
||||
s.RunForAllClients("Delete not existing channel", func(c client.Client) {
|
||||
notExistingChannelID := "not-existing-channel-ID"
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
args := []string{team.Id + ":" + notExistingChannelID}
|
||||
|
||||
printer.Clean()
|
||||
err := deleteChannelsCmdF(c, cmd, args)
|
||||
|
||||
arg := team.Id + ":" + notExistingChannelID
|
||||
var expected error
|
||||
expected = multierror.Append(expected, errors.New("unable to find channel '"+arg+"'"))
|
||||
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, expected.Error())
|
||||
|
||||
channel, err := s.th.App.GetChannel(s.th.Context, notExistingChannelID)
|
||||
|
||||
s.Require().Nil(channel)
|
||||
s.Require().NotNil(err)
|
||||
s.CheckErrorID(err, "app.channel.get.existing.app_error")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestChannelRenameCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
initChannelName := api4.GenerateTestChannelName()
|
||||
initChannelDisplayName := "dn_" + initChannelName
|
||||
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: initChannelName,
|
||||
DisplayName: initChannelDisplayName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForAllClients("Rename nonexistent channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentChannelName := api4.GenerateTestChannelName()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("name", "name", "")
|
||||
cmd.Flags().String("display-name", "name", "")
|
||||
|
||||
err := renameChannelCmdF(c, cmd, []string{s.th.BasicTeam.Id + ":" + nonexistentChannelName})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find channel from \"%s:%s\"", s.th.BasicTeam.Id, nonexistentChannelName), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Rename channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
newChannelName := api4.GenerateTestChannelName()
|
||||
newChannelDisplayName := "dn_" + newChannelName
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("name", newChannelName, "")
|
||||
cmd.Flags().String("display-name", newChannelDisplayName, "")
|
||||
|
||||
err := renameChannelCmdF(c, cmd, []string{s.th.BasicTeam.Id + ":" + channel.Id})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
printedChannel, ok := printer.GetLines()[0].(*model.Channel)
|
||||
s.Require().True(ok, "unexpected printer output type")
|
||||
|
||||
s.Require().Equal(newChannelName, printedChannel.Name)
|
||||
s.Require().Equal(newChannelDisplayName, printedChannel.DisplayName)
|
||||
|
||||
rchannel, err := s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(newChannelName, rchannel.Name)
|
||||
s.Require().Equal(newChannelDisplayName, rchannel.DisplayName)
|
||||
})
|
||||
|
||||
s.Run("Rename channel without permission", func() {
|
||||
printer.Clean()
|
||||
|
||||
channelInit, appErr := s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
newChannelName := api4.GenerateTestChannelName()
|
||||
newChannelDisplayName := "dn_" + newChannelName
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("name", newChannelName, "")
|
||||
cmd.Flags().String("display-name", newChannelDisplayName, "")
|
||||
|
||||
err := renameChannelCmdF(s.th.Client, cmd, []string{s.th.BasicTeam.Id + ":" + channel.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(fmt.Sprintf("cannot rename channel \"%s\", error: You do not have the appropriate permissions.", channelInit.Name), err.Error())
|
||||
|
||||
rchannel, err := s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(channelInit.Name, rchannel.Name)
|
||||
s.Require().Equal(channelInit.DisplayName, rchannel.DisplayName)
|
||||
})
|
||||
|
||||
s.Run("Rename channel with permission", func() {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr := s.th.App.AddChannelMember(s.th.Context, s.th.BasicUser.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
newChannelName := api4.GenerateTestChannelName()
|
||||
newChannelDisplayName := "dn_" + newChannelName
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("name", newChannelName, "")
|
||||
cmd.Flags().String("display-name", newChannelDisplayName, "")
|
||||
|
||||
err := renameChannelCmdF(s.th.Client, cmd, []string{s.th.BasicTeam.Id + ":" + channel.Id})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
printedChannel, ok := printer.GetLines()[0].(*model.Channel)
|
||||
s.Require().True(ok, "unexpected printer output type")
|
||||
|
||||
s.Require().Equal(newChannelName, printedChannel.Name)
|
||||
s.Require().Equal(newChannelDisplayName, printedChannel.DisplayName)
|
||||
|
||||
rchannel, err := s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(newChannelName, rchannel.Name)
|
||||
s.Require().Equal(newChannelDisplayName, rchannel.DisplayName)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestMoveChannelCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
initChannelName := api4.GenerateTestChannelName()
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: initChannelName,
|
||||
DisplayName: "dName_" + initChannelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForAllClients("Move nonexistent team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := moveChannelCmdF(c, &cobra.Command{}, []string{"test"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal(`unable to find destination team "test"`, err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Move existing channel to specified team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
testTeamName := api4.GenerateTestTeamName()
|
||||
var team *model.Team
|
||||
team, appErr = s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
Name: testTeamName,
|
||||
DisplayName: "dName_" + testTeamName,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
args := []string{team.Id, channel.Id}
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := moveChannelCmdF(c, cmd, args)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
actualChannel, ok := printer.GetLines()[0].(*model.Channel)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(channel.Name, actualChannel.Name)
|
||||
s.Require().Equal(team.Id, actualChannel.TeamId)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Moving team to non existing channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
args := []string{s.th.BasicTeam.Id, "no-channel"}
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
var expected error
|
||||
expected = multierror.Append(expected, fmt.Errorf("unable to find channel %q", "no-channel"))
|
||||
|
||||
err := moveChannelCmdF(c, cmd, args)
|
||||
|
||||
s.Require().EqualError(err, expected.Error())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Moving channel which is already moved to particular team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
initChannelName := api4.GenerateTestChannelName()
|
||||
channel, appErr = s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: initChannelName,
|
||||
DisplayName: "dName_" + initChannelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
args := []string{channel.TeamId, channel.Id}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := moveChannelCmdF(c, cmd, args)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Move existing channel to specified team should fail for client", func() {
|
||||
printer.Clean()
|
||||
|
||||
testTeamName := api4.GenerateTestTeamName()
|
||||
var team *model.Team
|
||||
team, appErr = s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
Name: testTeamName,
|
||||
DisplayName: "dName_" + testTeamName,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
args := []string{team.Id, channel.Id}
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := moveChannelCmdF(s.th.Client, cmd, args)
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find destination team %q", team.Id), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Move channel auto-adding users missing from the destination team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
testTeamName := api4.GenerateTestTeamName()
|
||||
team, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
Name: testTeamName,
|
||||
DisplayName: "dName_" + testTeamName,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
srcChannelName := api4.GenerateTestChannelName()
|
||||
srcChannel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: srcChannelName,
|
||||
DisplayName: "dName_" + srcChannelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// A channel member that belongs to the source team but not the destination team.
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, s.th.BasicTeam.Id, user.Id, "")
|
||||
s.Require().Nil(appErr)
|
||||
_, appErr = s.th.App.AddUserToChannel(s.th.Context, user, srcChannel, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Without the flag the move is rejected and the missing user is reported.
|
||||
err := moveChannelCmdF(c, &cobra.Command{}, []string{team.Id, srcChannel.Id})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), user.Username)
|
||||
s.Require().Contains(err.Error(), "--auto-add-users")
|
||||
|
||||
_, appErr = s.th.App.GetTeamMember(s.th.Context, team.Id, user.Id)
|
||||
s.Require().NotNil(appErr)
|
||||
|
||||
// With the flag the user is added to the destination team and the move succeeds.
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("auto-add-users", true, "")
|
||||
err = moveChannelCmdF(c, cmd, []string{team.Id, srcChannel.Id})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
movedChannel, appErr := s.th.App.GetChannel(s.th.Context, srcChannel.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal(team.Id, movedChannel.TeamId)
|
||||
|
||||
teamMember, appErr := s.th.App.GetTeamMember(s.th.Context, team.Id, user.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal(user.Id, teamMember.UserId)
|
||||
})
|
||||
}
|
||||
3429
server/cmd/mmctl/commands/channel_test.go
Normal file
3429
server/cmd/mmctl/commands/channel_test.go
Normal file
File diff suppressed because it is too large
Load Diff
232
server/cmd/mmctl/commands/channel_users.go
Normal file
232
server/cmd/mmctl/commands/channel_users.go
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ChannelUsersCmd = &cobra.Command{
|
||||
Use: "users",
|
||||
Short: "Management of channel users",
|
||||
}
|
||||
|
||||
var ChannelUsersAddCmd = &cobra.Command{
|
||||
Use: "add [channel] [users]",
|
||||
Short: "Add users to channel",
|
||||
Long: "Add some users to channel",
|
||||
Example: " channel users add myteam:mychannel user@example.com username",
|
||||
RunE: withClient(channelUsersAddCmdF),
|
||||
}
|
||||
|
||||
var ChannelUsersRemoveCmd = &cobra.Command{
|
||||
Use: "remove [channel] [users]",
|
||||
Short: "Remove users from channel",
|
||||
Long: "Remove some users from channel",
|
||||
Example: ` channel users remove myteam:mychannel user@example.com username
|
||||
channel users remove myteam:mychannel --all-users`,
|
||||
RunE: withClient(channelUsersRemoveCmdF),
|
||||
}
|
||||
|
||||
var ChannelUsersListCmd = &cobra.Command{
|
||||
Use: "list [channel]",
|
||||
Short: "List users of a channel",
|
||||
Long: "List the users belonging to a channel, printing each member's id, username, email and roles",
|
||||
Example: ` channel users list myteam:mychannel
|
||||
channel users list myteam:mychannel --all`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(channelUsersListCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ChannelUsersRemoveCmd.Flags().Bool("all-users", false, "Remove all users from the indicated channel.")
|
||||
|
||||
ChannelUsersListCmd.Flags().Int("page", 0, "Page number to fetch for the list of channel users")
|
||||
ChannelUsersListCmd.Flags().Int("per-page", DefaultPageSize, "Number of channel users to be fetched")
|
||||
ChannelUsersListCmd.Flags().Bool("all", false, "Fetch all channel users. --page flag will be ignored if provided")
|
||||
|
||||
ChannelUsersCmd.AddCommand(
|
||||
ChannelUsersAddCmd,
|
||||
ChannelUsersRemoveCmd,
|
||||
ChannelUsersListCmd,
|
||||
)
|
||||
|
||||
ChannelCmd.AddCommand(ChannelUsersCmd)
|
||||
}
|
||||
|
||||
func channelUsersAddCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return errors.New("not enough arguments")
|
||||
}
|
||||
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.Errorf("unable to find channel %q", args[0])
|
||||
}
|
||||
|
||||
var result *multierror.Error
|
||||
users := getUsersFromUserArgs(c, args[1:])
|
||||
for i, user := range users {
|
||||
err := addUserToChannel(c, channel, user, args[i+1])
|
||||
if err != nil {
|
||||
printer.PrintError(err.Error())
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func addUserToChannel(c client.Client, channel *model.Channel, user *model.User, userArg string) error {
|
||||
if user == nil {
|
||||
return fmt.Errorf("unable to find user %q", userArg)
|
||||
}
|
||||
if _, _, err := c.AddChannelMember(context.TODO(), channel.Id, user.Id); err != nil {
|
||||
return fmt.Errorf("unable to add %q to %q. Error: %w", userArg, channel.Name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func channelUsersRemoveCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
allUsers, _ := cmd.Flags().GetBool("all-users")
|
||||
|
||||
if allUsers && len(args) != 1 {
|
||||
return errors.New("individual users must not be specified in conjunction with the --all-users flag")
|
||||
}
|
||||
|
||||
if !allUsers && len(args) < 2 {
|
||||
return errors.New("you must specify some users to remove from the channel, or use the --all-users flag to remove them all")
|
||||
}
|
||||
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.Errorf("unable to find channel %q", args[0])
|
||||
}
|
||||
|
||||
var result *multierror.Error
|
||||
if allUsers {
|
||||
if err := removeAllUsersFromChannel(c, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
for i, user := range getUsersFromUserArgs(c, args[1:]) {
|
||||
err := removeUserFromChannel(c, channel, user, args[i+1])
|
||||
if err != nil {
|
||||
printer.PrintError(err.Error())
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func removeUserFromChannel(c client.Client, channel *model.Channel, user *model.User, userArg string) error {
|
||||
if user == nil {
|
||||
return fmt.Errorf("unable to find user %q", userArg)
|
||||
}
|
||||
if _, err := c.RemoveUserFromChannel(context.TODO(), channel.Id, user.Id); err != nil {
|
||||
return fmt.Errorf("unable to remove %q from %q. Error: %w", userArg, channel.Name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAllUsersFromChannel(c client.Client, channel *model.Channel) error {
|
||||
var result *multierror.Error
|
||||
members, _, err := c.GetChannelMembers(context.TODO(), channel.Id, 0, 10000, "")
|
||||
if err != nil {
|
||||
printer.PrintError("Unable to remove all users from " + channel.Name + ". Error: " + err.Error())
|
||||
return fmt.Errorf("unable to remove all users from %q: %w", channel.Name, err)
|
||||
}
|
||||
|
||||
for _, member := range members {
|
||||
if _, err := c.RemoveUserFromChannel(context.TODO(), channel.Id, member.UserId); err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("unable to remove %q from %q Error: %w", member.UserId, channel.Name, err))
|
||||
printer.PrintError("Unable to remove '" + member.UserId + "' from " + channel.Name + ". Error: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
// channelUserOut is the output format for a channel member, combining the
|
||||
// channel membership roles with the user's identifying details.
|
||||
type channelUserOut struct {
|
||||
Id string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Roles string `json:"roles"`
|
||||
}
|
||||
|
||||
func channelUsersListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.Errorf("unable to find channel %q", args[0])
|
||||
}
|
||||
|
||||
all, _ := cmd.Flags().GetBool("all")
|
||||
perPage, _ := cmd.Flags().GetInt("per-page")
|
||||
|
||||
var members model.ChannelMembers
|
||||
if all {
|
||||
var err error
|
||||
members, err = getPages(func(page, perPage int, etag string) ([]model.ChannelMember, *model.Response, error) {
|
||||
return c.GetChannelMembers(context.TODO(), channel.Id, page, perPage, etag)
|
||||
}, perPage)
|
||||
if err != nil {
|
||||
return errors.Errorf("unable to list users of channel %q: %s", channel.Name, err.Error())
|
||||
}
|
||||
} else {
|
||||
page, _ := cmd.Flags().GetInt("page")
|
||||
var err error
|
||||
members, _, err = c.GetChannelMembers(context.TODO(), channel.Id, page, perPage, "")
|
||||
if err != nil {
|
||||
return errors.Errorf("unable to list users of channel %q: %s", channel.Name, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(members) == 0 {
|
||||
printer.Print("No users found")
|
||||
return nil
|
||||
}
|
||||
|
||||
userIDs := make([]string, len(members))
|
||||
for i := range members {
|
||||
userIDs[i] = members[i].UserId
|
||||
}
|
||||
|
||||
users, _, err := c.GetUsersByIds(context.TODO(), userIDs)
|
||||
if err != nil {
|
||||
return errors.Errorf("unable to retrieve user details for channel %q: %s", channel.Name, err.Error())
|
||||
}
|
||||
|
||||
usersByID := make(map[string]*model.User, len(users))
|
||||
for _, user := range users {
|
||||
usersByID[user.Id] = user
|
||||
}
|
||||
|
||||
for _, member := range members {
|
||||
out := channelUserOut{
|
||||
Id: member.UserId,
|
||||
Roles: member.Roles,
|
||||
}
|
||||
if user, ok := usersByID[member.UserId]; ok {
|
||||
out.Username = user.Username
|
||||
out.Email = user.Email
|
||||
}
|
||||
printer.PrintT("{{.Id}}: {{.Username}} ({{.Email}}) {{.Roles}}", out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
376
server/cmd/mmctl/commands/channel_users_e2e_test.go
Normal file
376
server/cmd/mmctl/commands/channel_users_e2e_test.go
Normal file
@@ -0,0 +1,376 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestChannelUsersAddCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, s.th.BasicTeam.Id, user.Id, "")
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
channelName := api4.GenerateTestChannelName()
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: channelName,
|
||||
DisplayName: "dn_" + channelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForSystemAdminAndLocal("Add user to nonexistent channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentChannelName := "nonexistent"
|
||||
err := channelUsersAddCmdF(c, &cobra.Command{}, []string{nonexistentChannelName, user.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find channel %q", nonexistentChannelName), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Add user to nonexistent channel/Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr := s.th.App.AddChannelMember(s.th.Context, s.th.BasicUser.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr := s.th.App.RemoveUserFromChannel(s.th.Context, s.th.BasicUser.Id, s.th.SystemAdminUser.Id, channel)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
nonexistentChannelName := "nonexistent"
|
||||
err := channelUsersAddCmdF(s.th.Client, &cobra.Command{}, []string{nonexistentChannelName, user.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find channel %q", nonexistentChannelName), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Add nonexistent user to channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentUserName := "nonexistent"
|
||||
err := channelUsersAddCmdF(c, &cobra.Command{}, []string{channel.Id, nonexistentUserName})
|
||||
s.Require().ErrorContains(err, "unable to find user")
|
||||
s.Require().ErrorContains(err, nonexistentUserName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
})
|
||||
|
||||
s.Run("Add nonexistent user to channel/Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr := s.th.App.AddChannelMember(s.th.Context, s.th.BasicUser.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr := s.th.App.RemoveUserFromChannel(s.th.Context, s.th.BasicUser.Id, s.th.SystemAdminUser.Id, channel)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
nonexistentUserName := "nonexistent"
|
||||
err := channelUsersAddCmdF(s.th.Client, &cobra.Command{}, []string{channel.Id, nonexistentUserName})
|
||||
s.Require().ErrorContains(err, "unable to find user")
|
||||
s.Require().ErrorContains(err, nonexistentUserName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
})
|
||||
|
||||
s.Run("Add user to channel without permission/Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := channelUsersAddCmdF(s.th.Client, &cobra.Command{}, []string{channel.Id, user.Id})
|
||||
s.Require().ErrorContains(err, "unable to add")
|
||||
s.Require().ErrorContains(err, user.Id)
|
||||
s.Require().ErrorContains(err, channelName)
|
||||
s.Require().ErrorContains(err, "You do not have the appropriate permissions")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(fmt.Sprintf("unable to add %q to %q. Error: You do not have the appropriate permissions.", user.Id, channelName), printer.GetErrorLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Add user to channel/Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr := s.th.App.AddChannelMember(s.th.Context, s.th.BasicUser.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr = s.th.App.RemoveUserFromChannel(s.th.Context, s.th.BasicUser.Id, s.th.SystemAdminUser.Id, channel)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
err := channelUsersAddCmdF(s.th.Client, &cobra.Command{}, []string{channel.Id, user.Id})
|
||||
s.Require().Nil(err)
|
||||
defer func() {
|
||||
appErr = s.th.App.RemoveUserFromChannel(s.th.Context, user.Id, s.th.SystemAdminUser.Id, channel)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
members, appErr := s.th.App.GetChannelMembersByIds(s.th.Context, channel.Id, []string{user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(members, 1)
|
||||
s.Require().Equal(user.Id, (members)[0].UserId)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Add user to channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := channelUsersAddCmdF(c, &cobra.Command{}, []string{channel.Id, user.Id})
|
||||
s.Require().Nil(err)
|
||||
defer func() {
|
||||
appErr := s.th.App.RemoveUserFromChannel(s.th.Context, user.Id, s.th.SystemAdminUser.Id, channel)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
members, appErr := s.th.App.GetChannelMembersByIds(s.th.Context, channel.Id, []string{user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(members, 1)
|
||||
s.Require().Equal(user.Id, (members)[0].UserId)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestChannelUsersListCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, s.th.BasicTeam.Id, user.Id, "")
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
channelName := api4.GenerateTestChannelName()
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: channelName,
|
||||
DisplayName: "dn_" + channelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, appErr = s.th.App.AddChannelMember(s.th.Context, user.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForSystemAdminAndLocal("List users of a channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := channelUsersListCmdF(c, channelUsersListTestCmd(), []string{channel.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().NotEmpty(printer.GetLines())
|
||||
|
||||
var found bool
|
||||
for _, line := range printer.GetLines() {
|
||||
out, ok := line.(channelUserOut)
|
||||
s.Require().True(ok)
|
||||
if out.Id == user.Id {
|
||||
found = true
|
||||
s.Require().Equal(user.Username, out.Username)
|
||||
s.Require().Equal(user.Email, out.Email)
|
||||
s.Require().NotEmpty(out.Roles)
|
||||
}
|
||||
}
|
||||
s.Require().True(found, "expected the added user to be listed as a channel member")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("List all users of a channel with the --all flag", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := channelUsersListTestCmd()
|
||||
err := cmd.Flags().Set("all", "true")
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = channelUsersListCmdF(c, cmd, []string{channel.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
var found bool
|
||||
for _, line := range printer.GetLines() {
|
||||
out, ok := line.(channelUserOut)
|
||||
s.Require().True(ok)
|
||||
if out.Id == user.Id {
|
||||
found = true
|
||||
s.Require().Equal(user.Username, out.Username)
|
||||
s.Require().Equal(user.Email, out.Email)
|
||||
s.Require().NotEmpty(out.Roles)
|
||||
}
|
||||
}
|
||||
s.Require().True(found, "expected the added user to be listed as a channel member")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("List users of a nonexistent channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentChannelName := "nonexistent"
|
||||
err := channelUsersListCmdF(c, channelUsersListTestCmd(), []string{nonexistentChannelName})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find channel %q", nonexistentChannelName), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestChannelUsersRemoveCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, s.th.BasicTeam.Id, user.Id, "")
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
channelName := api4.GenerateTestChannelName()
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: channelName,
|
||||
DisplayName: "dn_" + channelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForSystemAdminAndLocal("Remove user from nonexistent channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentChannelName := "nonexistent"
|
||||
err := channelUsersRemoveCmdF(c, &cobra.Command{}, []string{nonexistentChannelName, user.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find channel %q", nonexistentChannelName), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Remove user from nonexistent channel/Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr = s.th.App.AddChannelMember(s.th.Context, s.th.BasicUser.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr = s.th.App.RemoveUserFromChannel(s.th.Context, s.th.BasicUser.Id, s.th.SystemAdminUser.Id, channel)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
nonexistentChannelName := "nonexistent"
|
||||
err := channelUsersRemoveCmdF(s.th.Client, &cobra.Command{}, []string{nonexistentChannelName, user.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find channel %q", nonexistentChannelName), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Remove nonexistent user from channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentUserName := "nonexistent"
|
||||
err := channelUsersRemoveCmdF(c, &cobra.Command{}, []string{channel.Id, nonexistentUserName})
|
||||
s.Require().ErrorContains(err, "unable to find user")
|
||||
s.Require().ErrorContains(err, nonexistentUserName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find user %q", nonexistentUserName), printer.GetErrorLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Remove nonexistent user from channel/Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr = s.th.App.AddChannelMember(s.th.Context, s.th.BasicUser.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr = s.th.App.RemoveUserFromChannel(s.th.Context, s.th.BasicUser.Id, s.th.SystemAdminUser.Id, channel)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
nonexistentUserName := "nonexistent"
|
||||
err := channelUsersRemoveCmdF(s.th.Client, &cobra.Command{}, []string{channel.Id, nonexistentUserName})
|
||||
s.Require().ErrorContains(err, "unable to find user")
|
||||
s.Require().ErrorContains(err, nonexistentUserName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find user %q", nonexistentUserName), printer.GetErrorLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Remove user from channel without permission/Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
var members model.ChannelMembers
|
||||
_, appErr = s.th.App.AddChannelMember(s.th.Context, user.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
members, appErr = s.th.App.GetChannelMembersByIds(s.th.Context, channel.Id, []string{user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(members, 1)
|
||||
s.Require().Equal(user.Id, (members)[0].UserId)
|
||||
|
||||
err := channelUsersRemoveCmdF(s.th.Client, &cobra.Command{}, []string{channel.Id, user.Id})
|
||||
s.Require().ErrorContains(err, "unable to remove")
|
||||
s.Require().ErrorContains(err, user.Id)
|
||||
s.Require().ErrorContains(err, channelName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("unable to remove %q from %q", user.Id, channelName))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "You do not have the appropriate permissions")
|
||||
})
|
||||
|
||||
s.Run("Remove user from channel/Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr = s.th.App.AddChannelMember(s.th.Context, s.th.BasicUser.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr = s.th.App.RemoveUserFromChannel(s.th.Context, s.th.BasicUser.Id, s.th.SystemAdminUser.Id, channel)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
var members model.ChannelMembers
|
||||
_, appErr = s.th.App.AddChannelMember(s.th.Context, user.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
members, appErr = s.th.App.GetChannelMembersByIds(s.th.Context, channel.Id, []string{user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(members, 1)
|
||||
s.Require().Equal(user.Id, (members)[0].UserId)
|
||||
|
||||
err := channelUsersRemoveCmdF(s.th.Client, &cobra.Command{}, []string{channel.Id, user.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
members, appErr = s.th.App.GetChannelMembersByIds(s.th.Context, channel.Id, []string{user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(members, 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Remove user from channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr = s.th.App.AddChannelMember(s.th.Context, user.Id, channel, app.ChannelMemberOpts{})
|
||||
s.Require().Nil(appErr)
|
||||
members, appErr := s.th.App.GetChannelMembersByIds(s.th.Context, channel.Id, []string{user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(members, 1)
|
||||
s.Require().Equal(user.Id, (members)[0].UserId)
|
||||
|
||||
err := channelUsersRemoveCmdF(c, &cobra.Command{}, []string{channel.Id, user.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
members, appErr = s.th.App.GetChannelMembersByIds(s.th.Context, channel.Id, []string{user.Id})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(members, 0)
|
||||
})
|
||||
}
|
||||
826
server/cmd/mmctl/commands/channel_users_test.go
Normal file
826
server/cmd/mmctl/commands/channel_users_test.go
Normal file
@@ -0,0 +1,826 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestChannelUsersAddCmdF() {
|
||||
channelArg := teamID + ":" + channelName
|
||||
mockTeam := model.Team{Id: teamID}
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelName}
|
||||
mockUser := model.User{Id: userID, Email: userEmail}
|
||||
|
||||
s.Run("Not enough command line parameters", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
// One argument provided.
|
||||
err := channelUsersAddCmdF(s.client, cmd, []string{channelArg})
|
||||
s.EqualError(err, "not enough arguments")
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// No arguments provided.
|
||||
err = channelUsersAddCmdF(s.client, cmd, []string{})
|
||||
s.EqualError(err, "not enough arguments")
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
s.Run("Add existing user to existing channel", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), userEmail, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(3)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
AddChannelMember(context.TODO(), channelID, userID).
|
||||
Return(&model.ChannelMember{}, &model.Response{}, nil).
|
||||
Times(2)
|
||||
err := channelUsersAddCmdF(s.client, cmd, []string{channelArg, userEmail})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
s.Run("Add existing user to nonexistent channel", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
// No channel is returned by client.
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersAddCmdF(s.client, cmd, []string{channelArg, userEmail})
|
||||
s.EqualError(err, fmt.Sprintf("unable to find channel %q", channelArg))
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
s.Run("Add existing user to channel owned by nonexistent team", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
// No team is returned by client.
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeamByName(context.TODO(), teamID, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersAddCmdF(s.client, cmd, []string{channelArg, userEmail})
|
||||
s.EqualError(err, fmt.Sprintf("unable to find channel %q", channelArg))
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
s.Run("Add multiple users, some nonexistent to existing channel", func() {
|
||||
printer.Clean()
|
||||
nilUserArg := "nonexistent-user"
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), nilUserArg, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), nilUserArg, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
err := channelUsersAddCmdF(s.client, cmd, []string{channelArg, nilUserArg, userEmail})
|
||||
s.Require().ErrorContains(err, "unable to find user")
|
||||
s.Require().ErrorContains(err, nilUserArg)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 1)
|
||||
})
|
||||
s.Run("Error adding existing user to existing channel", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
AddChannelMember(context.TODO(), channelID, userID).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
err := channelUsersAddCmdF(s.client, cmd, []string{channelArg, userEmail})
|
||||
s.Require().ErrorContains(err, "unable to add")
|
||||
s.Require().ErrorContains(err, userEmail)
|
||||
s.Require().ErrorContains(err, channelName)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 1)
|
||||
})
|
||||
}
|
||||
|
||||
func channelUsersListTestCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", DefaultPageSize, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestChannelUsersListCmd() {
|
||||
channelArg := teamID + ":" + channelName
|
||||
mockTeam := model.Team{Id: teamID}
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelName}
|
||||
mockUser := model.User{Id: userID, Username: "user1", Email: userEmail}
|
||||
mockUser2 := model.User{Id: userID + "2", Username: "user2", Email: userID + "2@example.com"}
|
||||
mockUser3 := model.User{Id: userID + "3", Username: "user3", Email: userID + "3@example.com"}
|
||||
member1 := model.ChannelMember{ChannelId: channelID, UserId: mockUser.Id, Roles: "channel_user"}
|
||||
member2 := model.ChannelMember{ChannelId: channelID, UserId: mockUser2.Id, Roles: "channel_user channel_admin"}
|
||||
member3 := model.ChannelMember{ChannelId: channelID, UserId: mockUser3.Id, Roles: "channel_user"}
|
||||
|
||||
s.Run("List users of a channel", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 0, DefaultPageSize, "").
|
||||
Return(model.ChannelMembers{member1, member2}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockUser.Id, mockUser2.Id}).
|
||||
Return([]*model.User{&mockUser, &mockUser2}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
out1 := printer.GetLines()[0].(channelUserOut)
|
||||
s.Require().Equal(mockUser.Id, out1.Id)
|
||||
s.Require().Equal(mockUser.Username, out1.Username)
|
||||
s.Require().Equal(mockUser.Email, out1.Email)
|
||||
s.Require().Equal("channel_user", out1.Roles)
|
||||
|
||||
out2 := printer.GetLines()[1].(channelUserOut)
|
||||
s.Require().Equal(mockUser2.Id, out2.Id)
|
||||
s.Require().Equal(mockUser2.Username, out2.Username)
|
||||
s.Require().Equal(mockUser2.Email, out2.Email)
|
||||
s.Require().Equal("channel_user channel_admin", out2.Roles)
|
||||
})
|
||||
|
||||
s.Run("List all users of a channel with the --all flag", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
err := cmd.Flags().Set("all", "true")
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
// The --all flag must keep paging until an empty page is returned,
|
||||
// accumulating members across every non-empty page.
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 0, DefaultPageSize, "").
|
||||
Return(model.ChannelMembers{member1, member2}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 1, DefaultPageSize, "").
|
||||
Return(model.ChannelMembers{member3}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 2, DefaultPageSize, "").
|
||||
Return(model.ChannelMembers{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockUser.Id, mockUser2.Id, mockUser3.Id}).
|
||||
Return([]*model.User{&mockUser, &mockUser2, &mockUser3}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 3)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
s.Require().Equal(mockUser.Id, printer.GetLines()[0].(channelUserOut).Id)
|
||||
s.Require().Equal(mockUser2.Id, printer.GetLines()[1].(channelUserOut).Id)
|
||||
s.Require().Equal(mockUser3.Id, printer.GetLines()[2].(channelUserOut).Id)
|
||||
})
|
||||
|
||||
s.Run("List users with custom --page and --per-page", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
s.Require().NoError(cmd.Flags().Set("page", "2"))
|
||||
s.Require().NoError(cmd.Flags().Set("per-page", "50"))
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
// The --page and --per-page flags must be threaded through to the API call.
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 2, 50, "").
|
||||
Return(model.ChannelMembers{member1}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockUser.Id}).
|
||||
Return([]*model.User{&mockUser}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(mockUser.Id, printer.GetLines()[0].(channelUserOut).Id)
|
||||
})
|
||||
|
||||
s.Run("List users renders id, username, email and roles in plain output", func() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
defer printer.SetFormat(printer.FormatJSON)
|
||||
cmd := channelUsersListTestCmd()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 0, DefaultPageSize, "").
|
||||
Return(model.ChannelMembers{member2}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockUser2.Id}).
|
||||
Return([]*model.User{&mockUser2}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
expected := fmt.Sprintf("%s: %s (%s) %s", mockUser2.Id, mockUser2.Username, mockUser2.Email, member2.Roles)
|
||||
s.Require().Equal(expected, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("List users when a member's user details are missing", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 0, DefaultPageSize, "").
|
||||
Return(model.ChannelMembers{member1, member2}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
// GetUsersByIds omits member2's user (e.g. deactivated/deleted).
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockUser.Id, mockUser2.Id}).
|
||||
Return([]*model.User{&mockUser}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
|
||||
out1 := printer.GetLines()[0].(channelUserOut)
|
||||
s.Require().Equal(mockUser.Id, out1.Id)
|
||||
s.Require().Equal(mockUser.Username, out1.Username)
|
||||
s.Require().Equal(mockUser.Email, out1.Email)
|
||||
|
||||
// The row for the missing user still lists its id and roles, with empty username/email.
|
||||
out2 := printer.GetLines()[1].(channelUserOut)
|
||||
s.Require().Equal(mockUser2.Id, out2.Id)
|
||||
s.Require().Equal("channel_user channel_admin", out2.Roles)
|
||||
s.Require().Empty(out2.Username)
|
||||
s.Require().Empty(out2.Email)
|
||||
})
|
||||
|
||||
s.Run("List all users when fetching members fails", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
s.Require().NoError(cmd.Flags().Set("all", "true"))
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 0, DefaultPageSize, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().ErrorContains(err, "unable to list users")
|
||||
s.Require().ErrorContains(err, channelName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List users of a nonexistent channel", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeamByName(context.TODO(), teamID, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().EqualError(err, fmt.Sprintf("unable to find channel %q", channelArg))
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List users of an empty channel", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 0, DefaultPageSize, "").
|
||||
Return(model.ChannelMembers{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal("No users found", printer.GetLines()[0])
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List users when fetching members fails", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 0, DefaultPageSize, "").
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().ErrorContains(err, "unable to list users")
|
||||
s.Require().ErrorContains(err, channelName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List users when fetching user details fails", func() {
|
||||
printer.Clean()
|
||||
cmd := channelUsersListTestCmd()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(&mockTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, teamID, "").
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), channelID, 0, DefaultPageSize, "").
|
||||
Return(model.ChannelMembers{member1}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.TODO(), []string{mockUser.Id}).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersListCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().ErrorContains(err, "unable to retrieve user details")
|
||||
s.Require().ErrorContains(err, channelName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestChannelUsersRemoveCmd() {
|
||||
mockUser := model.User{Id: userID, Email: userEmail}
|
||||
mockUser2 := model.User{Id: userID + "2", Email: userID + "2@example.com"}
|
||||
mockUser3 := model.User{Id: userID + "3", Email: userID + "3@example.com"}
|
||||
argsTeamChannel := teamName + ":" + channelName
|
||||
|
||||
s.Run("should remove user from channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{argsTeamChannel, userEmail}
|
||||
|
||||
foundTeam := &model.Team{
|
||||
Id: teamID,
|
||||
DisplayName: teamDisplayName,
|
||||
Name: teamName,
|
||||
}
|
||||
|
||||
foundChannel := &model.Channel{
|
||||
Id: channelID,
|
||||
Name: channelName,
|
||||
DisplayName: channelDisplayName,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamName, "").
|
||||
Return(foundTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, foundTeam.Id, "").
|
||||
Return(foundChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), userEmail, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveUserFromChannel(context.TODO(), foundChannel.Id, mockUser.Id).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersRemoveCmdF(s.client, cmd, args)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("should throw error if both --all-users flag and user email are passed", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("all-users", true, "Remove all users from the indicated channel.")
|
||||
args := []string{argsTeamChannel, userEmail}
|
||||
|
||||
err := channelUsersRemoveCmdF(s.client, cmd, args)
|
||||
s.Require().EqualError(err, "individual users must not be specified in conjunction with the --all-users flag")
|
||||
})
|
||||
|
||||
s.Run("should remove all users from channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("all-users", true, "Remove all users from the indicated channel.")
|
||||
args := []string{argsTeamChannel}
|
||||
|
||||
foundTeam := &model.Team{
|
||||
Id: teamID,
|
||||
DisplayName: teamDisplayName,
|
||||
Name: teamName,
|
||||
}
|
||||
|
||||
foundChannel := &model.Channel{
|
||||
Id: channelID,
|
||||
Name: channelName,
|
||||
DisplayName: channelDisplayName,
|
||||
}
|
||||
|
||||
mockMember1 := model.ChannelMember{ChannelId: channelID, UserId: mockUser.Id}
|
||||
mockMember2 := model.ChannelMember{ChannelId: channelID, UserId: mockUser2.Id}
|
||||
mockMember3 := model.ChannelMember{ChannelId: channelID, UserId: mockUser3.Id}
|
||||
mockChannelMembers := model.ChannelMembers{mockMember1, mockMember2, mockMember3}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamName, "").
|
||||
Return(foundTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), userEmail, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, foundTeam.Id, "").
|
||||
Return(foundChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), foundChannel.Id, 0, 10000, "").
|
||||
Return(mockChannelMembers, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveUserFromChannel(context.TODO(), foundChannel.Id, mockUser.Id).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveUserFromChannel(context.TODO(), foundChannel.Id, mockUser2.Id).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveUserFromChannel(context.TODO(), foundChannel.Id, mockUser3.Id).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersRemoveCmdF(s.client, cmd, args)
|
||||
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("should remove multiple users from channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{argsTeamChannel, userEmail, mockUser2.Email}
|
||||
|
||||
foundTeam := &model.Team{
|
||||
Id: teamID,
|
||||
DisplayName: teamDisplayName,
|
||||
Name: teamName,
|
||||
}
|
||||
|
||||
foundChannel := &model.Channel{
|
||||
Id: channelID,
|
||||
Name: channelName,
|
||||
DisplayName: channelDisplayName,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamName, "").
|
||||
Return(foundTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, foundTeam.Id, "").
|
||||
Return(foundChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), userEmail, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), mockUser2.Email, "").
|
||||
Return(&mockUser2, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveUserFromChannel(context.TODO(), foundChannel.Id, mockUser.Id).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveUserFromChannel(context.TODO(), foundChannel.Id, mockUser2.Id).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersRemoveCmdF(s.client, cmd, args)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("should remove all users from channel throws error", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("all-users", true, "Remove all users from the indicated channel.")
|
||||
args := []string{argsTeamChannel}
|
||||
|
||||
foundTeam := &model.Team{
|
||||
Id: teamID,
|
||||
DisplayName: teamDisplayName,
|
||||
Name: teamName,
|
||||
}
|
||||
|
||||
foundChannel := &model.Channel{
|
||||
Id: channelID,
|
||||
Name: channelName,
|
||||
DisplayName: channelDisplayName,
|
||||
}
|
||||
|
||||
mockMember1 := model.ChannelMember{ChannelId: channelID, UserId: mockUser.Id}
|
||||
mockChannelMembers := model.ChannelMembers{mockMember1}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamName, "").
|
||||
Return(foundTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, foundTeam.Id, "").
|
||||
Return(foundChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelMembers(context.TODO(), foundChannel.Id, 0, 10000, "").
|
||||
Return(mockChannelMembers, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveUserFromChannel(context.TODO(), foundChannel.Id, mockUser.Id).
|
||||
Return(&model.Response{StatusCode: http.StatusNotFound}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersRemoveCmdF(s.client, cmd, args)
|
||||
s.Require().ErrorContains(err, "unable to remove")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
})
|
||||
|
||||
s.Run("should remove user from channel throws error", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{argsTeamChannel, userEmail}
|
||||
|
||||
foundTeam := &model.Team{
|
||||
Id: teamID,
|
||||
DisplayName: teamDisplayName,
|
||||
Name: teamName,
|
||||
}
|
||||
|
||||
foundChannel := &model.Channel{
|
||||
Id: channelID,
|
||||
Name: channelName,
|
||||
DisplayName: channelDisplayName,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamName, "").
|
||||
Return(foundTeam, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelName, foundTeam.Id, "").
|
||||
Return(foundChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveUserFromChannel(context.TODO(), foundChannel.Id, mockUser.Id).
|
||||
Return(&model.Response{StatusCode: http.StatusNotFound}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := channelUsersRemoveCmdF(s.client, cmd, args)
|
||||
s.Require().ErrorContains(err, "unable to remove")
|
||||
s.Require().ErrorContains(err, userEmail)
|
||||
s.Require().ErrorContains(err, channelName)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
})
|
||||
}
|
||||
125
server/cmd/mmctl/commands/channelargs.go
Normal file
125
server/cmd/mmctl/commands/channelargs.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
)
|
||||
|
||||
const channelArgSeparator = ":"
|
||||
|
||||
func getChannelsFromChannelArgs(c client.Client, channelArgs []string) []*model.Channel {
|
||||
channels := make([]*model.Channel, 0, len(channelArgs))
|
||||
for _, channelArg := range channelArgs {
|
||||
channel := getChannelFromChannelArg(c, channelArg)
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
func parseChannelArg(channelArg string) (string, string) {
|
||||
result := strings.SplitN(channelArg, channelArgSeparator, 2)
|
||||
if len(result) == 1 {
|
||||
return "", channelArg
|
||||
}
|
||||
return result[0], result[1]
|
||||
}
|
||||
|
||||
func getChannelFromChannelArg(c client.Client, channelArg string) *model.Channel {
|
||||
teamArg, channelPart := parseChannelArg(channelArg)
|
||||
if teamArg == "" && channelPart == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkDots(channelPart) || checkSlash(channelPart) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var channel *model.Channel
|
||||
if teamArg != "" {
|
||||
team := getTeamFromTeamArg(c, teamArg)
|
||||
if team == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
channel, _, _ = c.GetChannelByNameIncludeDeleted(context.TODO(), channelPart, team.Id, "")
|
||||
}
|
||||
|
||||
if channel == nil {
|
||||
channel, _, _ = c.GetChannel(context.TODO(), channelPart)
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// getChannelsFromArgs obtains channels by the `channelArgs` parameter. It can return channels and errors
|
||||
// at the same time
|
||||
//
|
||||
//nolint:golint,unused
|
||||
func getChannelsFromArgs(c client.Client, channelArgs []string) ([]*model.Channel, error) {
|
||||
var channels []*model.Channel
|
||||
var result *multierror.Error
|
||||
for _, channelArg := range channelArgs {
|
||||
channel, err := getChannelFromArg(c, channelArg)
|
||||
if err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
continue
|
||||
}
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
return channels, result.ErrorOrNil()
|
||||
}
|
||||
|
||||
//nolint:golint,unused
|
||||
func getChannelFromArg(c client.Client, arg string) (*model.Channel, error) {
|
||||
teamArg, channelArg := parseChannelArg(arg)
|
||||
if teamArg == "" && channelArg == "" {
|
||||
return nil, fmt.Errorf("invalid channel argument %q", arg)
|
||||
}
|
||||
if checkDots(channelArg) || checkSlash(channelArg) {
|
||||
return nil, fmt.Errorf(`invalid channel argument. Cannot contain ".." nor "/"`)
|
||||
}
|
||||
var channel *model.Channel
|
||||
var response *model.Response
|
||||
if teamArg != "" {
|
||||
team, err := getTeamFromArg(c, teamArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channel, response, err = c.GetChannelByNameIncludeDeleted(context.TODO(), channelArg, team.Id, "")
|
||||
if err != nil {
|
||||
err = ExtractErrorFromResponse(response, err)
|
||||
var nfErr *NotFoundError
|
||||
var badRequestErr *BadRequestError
|
||||
if !errors.As(err, &nfErr) && !errors.As(err, &badRequestErr) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if channel != nil {
|
||||
return channel, nil
|
||||
}
|
||||
var err error
|
||||
channel, response, err = c.GetChannel(context.TODO(), channelArg)
|
||||
if err != nil {
|
||||
nErr := ExtractErrorFromResponse(response, err)
|
||||
var nfErr *NotFoundError
|
||||
var badRequestErr *BadRequestError
|
||||
if !errors.As(nErr, &nfErr) && !errors.As(nErr, &badRequestErr) {
|
||||
return nil, nErr
|
||||
}
|
||||
}
|
||||
if channel == nil {
|
||||
return nil, ErrEntityNotFound{Type: "channel", ID: arg}
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
114
server/cmd/mmctl/commands/channelargs_test.go
Normal file
114
server/cmd/mmctl/commands/channelargs_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestGetChannelArgs() {
|
||||
s.Run("channel not found", func() {
|
||||
notFoundChannel := "notfoundchannel"
|
||||
notFoundErr := errors.New("channel not found")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), notFoundChannel).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusNotFound}, notFoundErr).
|
||||
Times(1)
|
||||
|
||||
channels, err := getChannelsFromArgs(s.client, []string{notFoundChannel})
|
||||
s.Require().Empty(channels)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, fmt.Sprintf("1 error occurred:\n\t* channel %s not found\n\n", notFoundChannel))
|
||||
})
|
||||
s.Run("bad request", func() {
|
||||
badRequestChannel := "badrequest"
|
||||
badRequestErr := errors.New("channel bad request")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), badRequestChannel).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusBadRequest}, badRequestErr).
|
||||
Times(1)
|
||||
|
||||
channels, err := getChannelsFromArgs(s.client, []string{badRequestChannel})
|
||||
s.Require().Empty(channels)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, fmt.Sprintf("1 error occurred:\n\t* channel %s not found\n\n", badRequestChannel))
|
||||
})
|
||||
s.Run("forbidden", func() {
|
||||
forbidden := "forbidden"
|
||||
forbiddenErr := errors.New("channel forbidden")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), forbidden).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusForbidden}, forbiddenErr).
|
||||
Times(1)
|
||||
|
||||
channels, err := getChannelsFromArgs(s.client, []string{forbidden})
|
||||
s.Require().Empty(channels)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, "1 error occurred:\n\t* channel forbidden\n\n")
|
||||
})
|
||||
s.Run("internal server error", func() {
|
||||
errChannel := "internalServerError"
|
||||
internalServerErrorErr := errors.New("channel internalServerError")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), errChannel).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusInternalServerError}, internalServerErrorErr).
|
||||
Times(1)
|
||||
|
||||
channels, err := getChannelsFromArgs(s.client, []string{errChannel})
|
||||
s.Require().Empty(channels)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, "1 error occurred:\n\t* channel internalServerError\n\n")
|
||||
})
|
||||
s.Run("success", func() {
|
||||
successID := "success"
|
||||
successChannel := &model.Channel{Id: successID}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), successID).
|
||||
Return(successChannel, nil, nil).
|
||||
Times(1)
|
||||
|
||||
channels, summary := getChannelsFromArgs(s.client, []string{successID})
|
||||
s.Require().Nil(summary)
|
||||
s.Require().Len(channels, 1)
|
||||
s.Require().Equal(successChannel, channels[0])
|
||||
})
|
||||
|
||||
s.Run("success with team on channel", func() {
|
||||
channelID := "success"
|
||||
teamID := "myTeamID"
|
||||
successTeam := &model.Team{Id: teamID}
|
||||
successChannel := &model.Channel{Id: channelID}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetTeam(context.TODO(), teamID, "").
|
||||
Return(successTeam, nil, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannelByNameIncludeDeleted(context.TODO(), channelID, teamID, "").
|
||||
Return(successChannel, nil, nil).
|
||||
Times(1)
|
||||
|
||||
channels, summary := getChannelsFromArgs(s.client, []string{fmt.Sprintf("%v:%v", teamID, channelID)})
|
||||
s.Require().Nil(summary)
|
||||
s.Require().Len(channels, 1)
|
||||
s.Require().Equal(successChannel, channels[0])
|
||||
})
|
||||
}
|
||||
343
server/cmd/mmctl/commands/command.go
Normal file
343
server/cmd/mmctl/commands/command.go
Normal file
@@ -0,0 +1,343 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
var CommandCmd = &cobra.Command{
|
||||
Use: "command",
|
||||
Short: "Management of slash commands",
|
||||
}
|
||||
|
||||
var CommandCreateCmd = &cobra.Command{
|
||||
Use: "create [team]",
|
||||
Short: "Create a custom slash command",
|
||||
Long: `Create a custom slash command for the specified team.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Example: ` command create myteam --title MyCommand --description "My Command Description" --trigger-word mycommand --url http://localhost:8000/my-slash-handler --creator myusername --response-username my-bot-username --icon http://localhost:8000/my-slash-handler-bot-icon.png --autocomplete --post`,
|
||||
RunE: withClient(createCommandCmdF),
|
||||
}
|
||||
|
||||
var CommandListCmd = &cobra.Command{
|
||||
Use: "list [teams]",
|
||||
Short: "List all commands on specified teams.",
|
||||
Long: `List all commands on specified teams.`,
|
||||
Example: ` command list myteam`,
|
||||
RunE: withClient(listCommandCmdF),
|
||||
}
|
||||
|
||||
var CommandArchiveCmd = &cobra.Command{
|
||||
Use: "archive [commandID]",
|
||||
Short: "Archive a slash command",
|
||||
Long: `Archive a slash command. Commands can be specified by command ID.`,
|
||||
Example: ` command archive commandID`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(archiveCommandCmdF),
|
||||
}
|
||||
|
||||
var CommandModifyCmd = &cobra.Command{
|
||||
Use: "modify [commandID]",
|
||||
Short: "Modify a slash command",
|
||||
Long: `Modify a slash command. Commands can be specified by command ID.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Example: ` command modify commandID --title MyModifiedCommand --description "My Modified Command Description" --trigger-word mycommand --url http://localhost:8000/my-slash-handler --creator myusername --response-username my-bot-username --icon http://localhost:8000/my-slash-handler-bot-icon.png --autocomplete --post`,
|
||||
RunE: withClient(modifyCommandCmdF),
|
||||
}
|
||||
|
||||
var CommandMoveCmd = &cobra.Command{
|
||||
Use: "move [team] [commandID]",
|
||||
Short: "Move a slash command to a different team",
|
||||
Long: `Move a slash command to a different team. Commands can be specified by command ID.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Example: ` command move newteam commandID`,
|
||||
RunE: withClient(moveCommandCmdF),
|
||||
}
|
||||
|
||||
var CommandShowCmd = &cobra.Command{
|
||||
Use: "show [commandID]",
|
||||
Short: "Show a custom slash command",
|
||||
Long: `Show a custom slash command. Commands can be specified by command ID. Returns command ID, team ID, trigger word, display name and creator username.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Example: ` command show commandID`,
|
||||
RunE: withClient(showCommandCmdF),
|
||||
}
|
||||
|
||||
func addCommandFieldsFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String("title", "", "Command Title")
|
||||
cmd.Flags().String("description", "", "Command Description")
|
||||
cmd.Flags().String("trigger-word", "", "Command Trigger Word (required)")
|
||||
cmd.Flags().String("url", "", "Command Callback URL (required)")
|
||||
cmd.Flags().String("creator", "", "Command Creator's username, email or id (required)")
|
||||
cmd.Flags().String("response-username", "", "Command Response Username")
|
||||
cmd.Flags().String("icon", "", "Command Icon URL")
|
||||
cmd.Flags().Bool("autocomplete", false, "Show Command in autocomplete list")
|
||||
cmd.Flags().String("autocompleteDesc", "", "Short Command Description for autocomplete list")
|
||||
cmd.Flags().String("autocompleteHint", "", "Command Arguments displayed as help in autocomplete list")
|
||||
cmd.Flags().Bool("post", false, "Use POST method for Callback URL")
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmds := []*cobra.Command{CommandCreateCmd, CommandModifyCmd}
|
||||
for _, cmd := range cmds {
|
||||
addCommandFieldsFlags(cmd)
|
||||
}
|
||||
|
||||
_ = CommandCreateCmd.MarkFlagRequired("trigger-word")
|
||||
_ = CommandCreateCmd.MarkFlagRequired("url")
|
||||
_ = CommandCreateCmd.MarkFlagRequired("creator")
|
||||
|
||||
CommandCmd.AddCommand(
|
||||
CommandCreateCmd,
|
||||
CommandListCmd,
|
||||
CommandModifyCmd,
|
||||
CommandMoveCmd,
|
||||
CommandShowCmd,
|
||||
CommandArchiveCmd,
|
||||
)
|
||||
RootCmd.AddCommand(CommandCmd)
|
||||
}
|
||||
|
||||
func createCommandCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
team := getTeamFromTeamArg(c, args[0])
|
||||
if team == nil {
|
||||
return errors.New("unable to find team '" + args[0] + "'")
|
||||
}
|
||||
|
||||
// get the creator
|
||||
creator, _ := cmd.Flags().GetString("creator")
|
||||
user := getUserFromUserArg(c, creator)
|
||||
if user == nil {
|
||||
return errors.New("unable to find user '" + creator + "'")
|
||||
}
|
||||
|
||||
title, _ := cmd.Flags().GetString("title")
|
||||
description, _ := cmd.Flags().GetString("description")
|
||||
trigger, _ := cmd.Flags().GetString("trigger-word")
|
||||
|
||||
if strings.HasPrefix(trigger, "/") {
|
||||
return errors.New("a trigger word cannot begin with a /")
|
||||
}
|
||||
if strings.Contains(trigger, " ") {
|
||||
return errors.New("a trigger word must not contain spaces")
|
||||
}
|
||||
|
||||
url, _ := cmd.Flags().GetString("url")
|
||||
responseUsername, _ := cmd.Flags().GetString("response-username")
|
||||
icon, _ := cmd.Flags().GetString("icon")
|
||||
autocomplete, _ := cmd.Flags().GetBool("autocomplete")
|
||||
autocompleteDesc, _ := cmd.Flags().GetString("autocompleteDesc")
|
||||
autocompleteHint, _ := cmd.Flags().GetString("autocompleteHint")
|
||||
post, errp := cmd.Flags().GetBool("post")
|
||||
method := "P"
|
||||
if errp != nil || !post {
|
||||
method = "G"
|
||||
}
|
||||
|
||||
newCommand := &model.Command{
|
||||
CreatorId: user.Id,
|
||||
TeamId: team.Id,
|
||||
Trigger: trigger,
|
||||
Method: method,
|
||||
Username: responseUsername,
|
||||
IconURL: icon,
|
||||
AutoComplete: autocomplete,
|
||||
AutoCompleteDesc: autocompleteDesc,
|
||||
AutoCompleteHint: autocompleteHint,
|
||||
DisplayName: title,
|
||||
Description: description,
|
||||
URL: url,
|
||||
}
|
||||
|
||||
createdCommand, _, err := c.CreateCommand(context.TODO(), newCommand)
|
||||
if err != nil {
|
||||
return errors.New("unable to create command '" + newCommand.DisplayName + "'. " + err.Error())
|
||||
}
|
||||
|
||||
printer.PrintT("created command {{.DisplayName}}", createdCommand)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listCommandCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
var teams []*model.Team
|
||||
if len(args) < 1 {
|
||||
teamList, err := getPages(func(page, numPerPage int, etag string) ([]*model.Team, *model.Response, error) {
|
||||
return c.GetAllTeams(context.TODO(), etag, page, numPerPage)
|
||||
}, DefaultPageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
teams = teamList
|
||||
} else {
|
||||
teams = getTeamsFromTeamArgs(c, args)
|
||||
}
|
||||
|
||||
var errs *multierror.Error
|
||||
for i, team := range teams {
|
||||
if team == nil {
|
||||
printer.PrintError("Unable to find team '" + args[i] + "'")
|
||||
errs = multierror.Append(errs, fmt.Errorf("unable to find team '%s'", args[i]))
|
||||
continue
|
||||
}
|
||||
commands, _, err := c.ListCommands(context.TODO(), team.Id, true)
|
||||
if err != nil {
|
||||
printer.PrintError("Unable to list commands for '" + team.Id + "'")
|
||||
errs = multierror.Append(errs, fmt.Errorf("unable to list commands for '%s': %w", team.Id, err))
|
||||
continue
|
||||
}
|
||||
for _, command := range commands {
|
||||
printer.PrintT("{{.Id}}: {{.DisplayName}} (team: "+team.Name+")", command)
|
||||
}
|
||||
}
|
||||
return errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
func archiveCommandCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
resp, err := c.DeleteCommand(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return errors.New("Unable to archive command '" + args[0] + "' error: " + err.Error())
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
printer.PrintT("Status: {{.status}}", map[string]any{"status": "ok"})
|
||||
} else {
|
||||
printer.PrintT("Status: {{.status}}", map[string]any{"status": "error"})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func modifyCommandCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
command := getCommandFromCommandArg(c, args[0])
|
||||
if command == nil {
|
||||
return fmt.Errorf("unable to find command '%s'", args[0])
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
if flags.Changed("title") {
|
||||
command.DisplayName, _ = flags.GetString("title")
|
||||
}
|
||||
if flags.Changed("description") {
|
||||
command.Description, _ = flags.GetString("description")
|
||||
}
|
||||
if flags.Changed("trigger-word") {
|
||||
trigger, _ := flags.GetString("trigger-word")
|
||||
if strings.HasPrefix(trigger, "/") {
|
||||
return errors.New("a trigger word cannot begin with a /")
|
||||
}
|
||||
if strings.Contains(trigger, " ") {
|
||||
return errors.New("a trigger word must not contain spaces")
|
||||
}
|
||||
command.Trigger = trigger
|
||||
}
|
||||
if flags.Changed("url") {
|
||||
command.URL, _ = flags.GetString("url")
|
||||
}
|
||||
if flags.Changed("creator") {
|
||||
creator, _ := flags.GetString("creator")
|
||||
user := getUserFromUserArg(c, creator)
|
||||
if user == nil {
|
||||
return fmt.Errorf("unable to find user '%s'", creator)
|
||||
}
|
||||
command.CreatorId = user.Id
|
||||
}
|
||||
if flags.Changed("response-username") {
|
||||
command.Username, _ = flags.GetString("response-username")
|
||||
}
|
||||
if flags.Changed("icon") {
|
||||
command.IconURL, _ = flags.GetString("icon")
|
||||
}
|
||||
if flags.Changed("autocomplete") {
|
||||
command.AutoComplete, _ = flags.GetBool("autocomplete")
|
||||
}
|
||||
if flags.Changed("autocompleteDesc") {
|
||||
command.AutoCompleteDesc, _ = flags.GetString("autocompleteDesc")
|
||||
}
|
||||
if flags.Changed("autocompleteHint") {
|
||||
command.AutoCompleteHint, _ = flags.GetString("autocompleteHint")
|
||||
}
|
||||
if flags.Changed("post") {
|
||||
post, _ := flags.GetBool("post")
|
||||
if post {
|
||||
command.Method = "P"
|
||||
} else {
|
||||
command.Method = "G"
|
||||
}
|
||||
}
|
||||
|
||||
modifiedCommand, _, err := c.UpdateCommand(context.TODO(), command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to modify command '%s': %w", command.DisplayName, err)
|
||||
}
|
||||
|
||||
printer.PrintT("modified command {{.DisplayName}}", modifiedCommand)
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveCommandCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
newTeam := getTeamFromTeamArg(c, args[0])
|
||||
if newTeam == nil {
|
||||
return fmt.Errorf("unable to find team '%s'", args[0])
|
||||
}
|
||||
|
||||
command := getCommandFromCommandArg(c, args[1])
|
||||
if command == nil {
|
||||
return fmt.Errorf("unable to find command '%s'", args[1])
|
||||
}
|
||||
|
||||
resp, err := c.MoveCommand(context.TODO(), newTeam.Id, command.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to move command '%s': %w", command.Id, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
printer.PrintT("Status: {{.status}}", map[string]any{"status": "ok"})
|
||||
} else {
|
||||
printer.PrintT("Status: {{.status}}", map[string]any{"status": "error"})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func showCommandCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
command := getCommandFromCommandArg(c, args[0])
|
||||
if command == nil {
|
||||
return fmt.Errorf("unable to find command '%s'", args[0])
|
||||
}
|
||||
|
||||
template :=
|
||||
`teamId: {{.TeamId}}
|
||||
title: {{.DisplayName}}
|
||||
description: {{.Description}}
|
||||
trigger-word: {{.Trigger}}
|
||||
URL: {{.URL}}
|
||||
creatorId: {{.CreatorId}}
|
||||
response-username: {{.Username}}
|
||||
iconURL: {{.IconURL}}
|
||||
autoComplete: {{.AutoComplete}}
|
||||
autoCompleteDesc: {{.AutoCompleteDesc}}
|
||||
autoCompleteHint: {{.AutoCompleteHint}}
|
||||
method: {{.Method}}`
|
||||
|
||||
printer.PrintT(template, command)
|
||||
return nil
|
||||
}
|
||||
378
server/cmd/mmctl/commands/command_e2e_test.go
Normal file
378
server/cmd/mmctl/commands/command_e2e_test.go
Normal file
@@ -0,0 +1,378 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestListCommandCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForAllClients("List commands for a non existing team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentTeamID := "nonexistent-team-id"
|
||||
|
||||
err := listCommandCmdF(c, &cobra.Command{}, []string{nonexistentTeamID})
|
||||
s.Require().Error(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 1)
|
||||
s.Equal("Unable to find team '"+nonexistentTeamID+"'", printer.GetErrorLines()[0])
|
||||
})
|
||||
|
||||
s.RunForAllClients("List commands for a specific team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
team, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
DisplayName: "dn_" + model.NewId(),
|
||||
Name: api4.GenerateTestTeamName(),
|
||||
Email: s.th.BasicUser.Email,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, team.Id, s.th.BasicUser.Id, "")
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
command, appErr := s.th.App.CreateCommand(&model.Command{
|
||||
DisplayName: "command",
|
||||
CreatorId: s.th.BasicUser.Id,
|
||||
TeamId: team.Id,
|
||||
URL: "http://localhost:8000/example",
|
||||
Method: model.CommandMethodGet,
|
||||
Trigger: "trigger",
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr = s.th.App.DeleteCommand(command.Id)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
err := listCommandCmdF(c, &cobra.Command{}, []string{team.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Equal(command, printer.GetLines()[0])
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List all commands from all teams", func() {
|
||||
// add team1
|
||||
team1, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
DisplayName: "dn_" + model.NewId(),
|
||||
Name: api4.GenerateTestTeamName(),
|
||||
Email: s.th.BasicUser.Email,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, team1.Id, s.th.BasicUser.Id, "")
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
command1, appErr := s.th.App.CreateCommand(&model.Command{
|
||||
DisplayName: "command1",
|
||||
CreatorId: s.th.BasicUser.Id,
|
||||
TeamId: team1.Id,
|
||||
URL: "http://localhost:8000/example",
|
||||
Method: model.CommandMethodGet,
|
||||
Trigger: "trigger",
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr = s.th.App.DeleteCommand(command1.Id)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
// add team 2
|
||||
team2, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
DisplayName: "dn_" + model.NewId(),
|
||||
Name: api4.GenerateTestTeamName(),
|
||||
Email: s.th.BasicUser.Email,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, team2.Id, s.th.BasicUser.Id, "")
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
command2, appErr := s.th.App.CreateCommand(&model.Command{
|
||||
DisplayName: "command2",
|
||||
CreatorId: s.th.BasicUser.Id,
|
||||
TeamId: team2.Id,
|
||||
URL: "http://localhost:8000/example",
|
||||
Method: model.CommandMethodGet,
|
||||
Trigger: "trigger",
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr = s.th.App.DeleteCommand(command2.Id)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
s.RunForSystemAdminAndLocal("Run list command", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := listCommandCmdF(c, &cobra.Command{}, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 2)
|
||||
s.ElementsMatch([]*model.Command{command1, command2}, printer.GetLines())
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
})
|
||||
|
||||
s.Run("List commands for a specific team without permission", func() {
|
||||
printer.Clean()
|
||||
|
||||
team, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
DisplayName: "dn_" + model.NewId(),
|
||||
Name: api4.GenerateTestTeamName(),
|
||||
Email: s.th.BasicUser.Email,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
command, appErr := s.th.App.CreateCommand(&model.Command{
|
||||
DisplayName: "command",
|
||||
CreatorId: s.th.BasicUser.Id,
|
||||
TeamId: team.Id,
|
||||
URL: "http://localhost:8000/example",
|
||||
Method: model.CommandMethodGet,
|
||||
Trigger: "trigger",
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
appErr = s.th.App.DeleteCommand(command.Id)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
err := listCommandCmdF(s.th.Client, &cobra.Command{}, []string{team.Id})
|
||||
s.Require().Error(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 1)
|
||||
s.Equal("Unable to find team '"+team.Id+"'", printer.GetErrorLines()[0])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestArchiveCommandCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
teamOfBasicUser, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
DisplayName: "dn_" + model.NewId(),
|
||||
Name: api4.GenerateTestTeamName(),
|
||||
Email: s.th.BasicUser.Email,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, teamOfBasicUser.Id, s.th.BasicUser.Id, "")
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForAllClients("Archive nonexistent command", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentCommandID := "nonexistent-command-id"
|
||||
|
||||
err := archiveCommandCmdF(c, &cobra.Command{}, []string{nonexistentCommandID})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("Unable to archive command '%s' error: Sorry, we could not find the page., There doesn't appear to be an api call for the url='/api/v4/commands/nonexistent-command-id'. Typo? are you missing a team_id or user_id as part of the url?", nonexistentCommandID), err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForAllClients("Archive command", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
command, appErr := s.th.App.CreateCommand(&model.Command{
|
||||
TeamId: teamOfBasicUser.Id,
|
||||
DisplayName: "command",
|
||||
Description: "command",
|
||||
Trigger: api4.GenerateTestID(),
|
||||
URL: "http://localhost:8000/example",
|
||||
CreatorId: s.th.BasicUser.Id,
|
||||
Username: s.th.BasicUser.Username,
|
||||
IconURL: "http://localhost:8000/icon.ico",
|
||||
Method: model.CommandMethodGet,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := archiveCommandCmdF(c, &cobra.Command{}, []string{command.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(map[string]any{"status": "ok"}, printer.GetLines()[0])
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
rcommand, err := s.th.App.GetCommand(command.Id)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Nil(rcommand)
|
||||
s.Require().Contains(err.Error(), "SqlCommandStore.Get: Command does not exist., ")
|
||||
})
|
||||
|
||||
s.Run("Archive command without permission", func() {
|
||||
printer.Clean()
|
||||
|
||||
teamOfAdminUser, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
DisplayName: "dn_" + model.NewId(),
|
||||
Name: api4.GenerateTestTeamName(),
|
||||
Email: s.th.SystemAdminUser.Email,
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
command, appErr := s.th.App.CreateCommand(&model.Command{
|
||||
TeamId: teamOfAdminUser.Id,
|
||||
DisplayName: "command",
|
||||
Description: "command",
|
||||
Trigger: api4.GenerateTestID(),
|
||||
URL: "http://localhost:8000/example",
|
||||
CreatorId: s.th.SystemAdminUser.Id,
|
||||
Username: s.th.SystemAdminUser.Username,
|
||||
IconURL: "http://localhost:8000/icon.ico",
|
||||
Method: model.CommandMethodGet,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := archiveCommandCmdF(s.th.Client, &cobra.Command{}, []string{command.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(fmt.Sprintf("Unable to archive command '%s' error: Unable to get the command.", command.Id), err.Error())
|
||||
|
||||
rcommand, err := s.th.App.GetCommand(command.Id)
|
||||
s.Require().Nil(err)
|
||||
s.Require().NotNil(rcommand)
|
||||
s.Require().Equal(int64(0), rcommand.DeleteAt)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestModifyCommandCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
// create new command
|
||||
newCmd := &model.Command{
|
||||
CreatorId: s.th.BasicUser.Id,
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
URL: "http://nowhere.com",
|
||||
Method: model.CommandMethodPost,
|
||||
Trigger: "trigger",
|
||||
}
|
||||
|
||||
command, _, _ := s.th.SystemAdminClient.CreateCommand(context.Background(), newCmd)
|
||||
index := 0
|
||||
s.RunForSystemAdminAndLocal("modifyCommandCmdF", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
// Reset the cmd and parse to force Flag.Changed to be true.
|
||||
cmd := CommandModifyCmd
|
||||
cmd.ResetFlags()
|
||||
addCommandFieldsFlags(cmd)
|
||||
url := fmt.Sprintf("%s-%d", command.URL, index)
|
||||
index++
|
||||
err := cmd.ParseFlags([]string{
|
||||
command.Id,
|
||||
"--url=" + url,
|
||||
})
|
||||
s.Require().Nil(err)
|
||||
|
||||
err = modifyCommandCmdF(c, cmd, []string{command.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
changedCommand, err := s.th.App.GetCommand(command.Id)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(url, changedCommand.URL)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("modifyCommandCmdF for command that does not exist", func(c client.Client) {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := modifyCommandCmdF(c, cmd, []string{"nothing"})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("unable to find command 'nothing'", err.Error())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("modifyCommandCmdF with a space in trigger word", func(c client.Client) {
|
||||
printer.Clean()
|
||||
// Reset the cmd and parse to force Flag.Changed to be true.
|
||||
cmd := CommandModifyCmd
|
||||
cmd.ResetFlags()
|
||||
addCommandFieldsFlags(cmd)
|
||||
err := cmd.ParseFlags([]string{
|
||||
command.Id,
|
||||
"--trigger-word=modified with space",
|
||||
})
|
||||
s.Require().Nil(err)
|
||||
|
||||
err = modifyCommandCmdF(c, cmd, []string{command.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.EqualError(err, "a trigger word must not contain spaces")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestShowCommandCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("Show non existent cmd", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := showCommandCmdF(c, &cobra.Command{}, []string{"nonexistent-command-id"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal("unable to find command 'nonexistent-command-id'", err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Show commands with cmd id", func(c client.Client) {
|
||||
// create new command
|
||||
printer.Clean()
|
||||
newCmd := &model.Command{
|
||||
CreatorId: s.th.BasicUser.Id,
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
URL: "http://nowhere.com",
|
||||
Method: model.CommandMethodPost,
|
||||
Trigger: model.NewRandomString(6),
|
||||
}
|
||||
|
||||
command, _, err := c.CreateCommand(context.Background(), newCmd)
|
||||
s.Require().NoError(err)
|
||||
err = showCommandCmdF(c, &cobra.Command{}, []string{command.Id})
|
||||
s.Require().NoError(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal(command, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Show commands with team:trigger", func(c client.Client) {
|
||||
// create new command
|
||||
printer.Clean()
|
||||
trigger := model.NewRandomString(6)
|
||||
newCmd := &model.Command{
|
||||
CreatorId: s.th.BasicUser.Id,
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
URL: "http://nowhere.com",
|
||||
Method: model.CommandMethodPost,
|
||||
Trigger: trigger,
|
||||
}
|
||||
|
||||
command, _, err := c.CreateCommand(context.Background(), newCmd)
|
||||
|
||||
s.Require().NoError(err)
|
||||
err = showCommandCmdF(c, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + trigger})
|
||||
s.Require().NoError(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
outputCmd := printer.GetLines()[0].(*model.Command)
|
||||
s.Require().Equal(command.TeamId, outputCmd.TeamId)
|
||||
s.Require().Equal(command.Trigger, outputCmd.Trigger)
|
||||
})
|
||||
}
|
||||
1059
server/cmd/mmctl/commands/command_test.go
Normal file
1059
server/cmd/mmctl/commands/command_test.go
Normal file
File diff suppressed because it is too large
Load Diff
56
server/cmd/mmctl/commands/commandargs.go
Normal file
56
server/cmd/mmctl/commands/commandargs.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// getCommandFromCommandArg retrieves a Command by command id or team:trigger.
|
||||
func getCommandFromCommandArg(c client.Client, commandArg string) *model.Command {
|
||||
if checkSlash(commandArg) {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := getCommandFromTeamTrigger(c, commandArg)
|
||||
if cmd == nil {
|
||||
cmd, _, _ = c.GetCommandById(context.TODO(), commandArg)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// getCommandFromTeamTrigger retrieves a Command via team:trigger syntax.
|
||||
func getCommandFromTeamTrigger(c client.Client, teamTrigger string) *model.Command {
|
||||
arr := strings.Split(teamTrigger, ":")
|
||||
if len(arr) != 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
team, _, _ := c.GetTeamByName(context.TODO(), arr[0], "")
|
||||
if team == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
trigger := arr[1]
|
||||
if trigger == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
list, _, _ := c.ListCommands(context.TODO(), team.Id, false)
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, cmd := range list {
|
||||
if cmd.Trigger == trigger {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
79
server/cmd/mmctl/commands/completion.go
Normal file
79
server/cmd/mmctl/commands/completion.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func noCompletion(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
type validateArgsFn func(ctx context.Context, c client.Client, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
|
||||
func validateArgsWithClient(fn validateArgsFn) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shellCompleteTimeout)
|
||||
defer cancel()
|
||||
|
||||
c, _, _, err := getClient(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
return fn(ctx, c, cmd, args, toComplete)
|
||||
}
|
||||
}
|
||||
|
||||
type fetcher[T any] func(ctx context.Context, c client.Client, page int, perPage int) ([]T, *model.Response, error) // fetcher calls the Mattermost API to fetch a list of entities T.
|
||||
type matcher[T any] func(t T) []string // matcher returns list of field that are T uses for shell completion.
|
||||
|
||||
func fetchAndComplete[T any](f fetcher[T], m matcher[T]) validateArgsFn {
|
||||
return func(ctx context.Context, c client.Client, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res := []string{}
|
||||
|
||||
if toComplete == "" {
|
||||
return res, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
var page int
|
||||
for {
|
||||
entities, _, err := f(ctx, c, page, DefaultPageSize)
|
||||
if err != nil {
|
||||
// Return what we got so far
|
||||
return res, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
for _, e := range entities {
|
||||
for _, field := range m(e) {
|
||||
if strings.HasPrefix(field, toComplete) {
|
||||
res = append(res, field)
|
||||
|
||||
// Only complete one field per entity.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(res) > shellCompletionMaxItems {
|
||||
res = res[:shellCompletionMaxItems]
|
||||
break
|
||||
}
|
||||
|
||||
if len(entities) < DefaultPageSize {
|
||||
break
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
return res, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
157
server/cmd/mmctl/commands/completion_test.go
Normal file
157
server/cmd/mmctl/commands/completion_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
)
|
||||
|
||||
func TestFetchAndComplete(t *testing.T) {
|
||||
type user struct {
|
||||
name string
|
||||
position string
|
||||
}
|
||||
|
||||
createUsers := func(page, perPage int) []user {
|
||||
ret := []user{}
|
||||
for i := perPage * page; i < perPage*(page+1); i++ {
|
||||
ret = append(ret, user{
|
||||
name: fmt.Sprintf("name_%d", i),
|
||||
position: fmt.Sprintf("position_%d", i),
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
listNames := func(n int) []string {
|
||||
ret := []string{}
|
||||
for i := range n {
|
||||
ret = append(ret, fmt.Sprintf("name_%d", i))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
fetcher func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error)
|
||||
matcher func(t user) []string
|
||||
toComplete string
|
||||
ExpectedCompletion []string
|
||||
ExpectedDirective cobra.ShellCompDirective // Defaults to cobra.ShellCompDirectiveNoFileComp
|
||||
}{
|
||||
"empty query leads to empty result": {
|
||||
fetcher: func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error) {
|
||||
return []user{{name: "bob"}, {name: "alice"}}, nil, nil
|
||||
},
|
||||
matcher: func(t user) []string {
|
||||
return []string{t.name}
|
||||
},
|
||||
toComplete: "",
|
||||
ExpectedCompletion: []string{},
|
||||
},
|
||||
"no matches": {
|
||||
fetcher: func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error) {
|
||||
return []user{{name: "bob"}, {name: "alice"}}, nil, nil
|
||||
},
|
||||
matcher: func(t user) []string {
|
||||
return []string{t.name}
|
||||
},
|
||||
toComplete: "x",
|
||||
ExpectedCompletion: []string{},
|
||||
},
|
||||
"one element matches": {
|
||||
fetcher: func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error) {
|
||||
return []user{{name: "bob"}, {name: "alice"}}, nil, nil
|
||||
},
|
||||
matcher: func(t user) []string {
|
||||
return []string{t.name}
|
||||
},
|
||||
toComplete: "b",
|
||||
ExpectedCompletion: []string{"bob"},
|
||||
},
|
||||
"two element matches": {
|
||||
fetcher: func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error) {
|
||||
return []user{{name: "anne"}, {name: "alice"}}, nil, nil
|
||||
},
|
||||
matcher: func(t user) []string {
|
||||
return []string{t.name}
|
||||
},
|
||||
toComplete: "a",
|
||||
ExpectedCompletion: []string{"anne", "alice"},
|
||||
},
|
||||
"only match one fields per element": {
|
||||
fetcher: func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error) {
|
||||
return []user{{name: "bob", position: "backend"}, {name: "alice"}}, nil, nil
|
||||
},
|
||||
matcher: func(t user) []string {
|
||||
return []string{t.name, t.position}
|
||||
},
|
||||
toComplete: "b",
|
||||
ExpectedCompletion: []string{"bob"},
|
||||
},
|
||||
"error ignored returns": {
|
||||
fetcher: func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error) {
|
||||
return []user{{name: "bob", position: "backend"}, {name: "alice"}}, nil, errors.New("some error")
|
||||
},
|
||||
matcher: func(t user) []string {
|
||||
return []string{t.name, t.position}
|
||||
},
|
||||
toComplete: "b",
|
||||
ExpectedCompletion: []string{},
|
||||
},
|
||||
"limit to 50": {
|
||||
fetcher: func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error) {
|
||||
return createUsers(0, 200), nil, nil
|
||||
},
|
||||
matcher: func(t user) []string {
|
||||
return []string{t.name}
|
||||
},
|
||||
toComplete: "name",
|
||||
ExpectedCompletion: listNames(50),
|
||||
},
|
||||
"request multipile pages": {
|
||||
fetcher: func(ctx context.Context, c client.Client, page int, perPage int) ([]user, *model.Response, error) {
|
||||
return createUsers(page, perPage), nil, nil
|
||||
},
|
||||
matcher: func(t user) []string {
|
||||
return []string{t.name}
|
||||
},
|
||||
toComplete: "name_4",
|
||||
ExpectedCompletion: []string{
|
||||
"name_4", "name_40", "name_41", "name_42", "name_43", "name_44", "name_45", "name_46", "name_47", "name_48", "name_49",
|
||||
"name_400", "name_401", "name_402", "name_403", "name_404", "name_405", "name_406", "name_407", "name_408", "name_409", "name_410", "name_411", "name_412",
|
||||
"name_413", "name_414", "name_415", "name_416", "name_417", "name_418", "name_419", "name_420", "name_421", "name_422", "name_423", "name_424", "name_425",
|
||||
"name_426", "name_427", "name_428", "name_429", "name_430", "name_431", "name_432", "name_433", "name_434", "name_435", "name_436", "name_437", "name_438",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
comp, directive := fetchAndComplete(tc.fetcher, tc.matcher)(context.Background(), nil, nil, nil, tc.toComplete)
|
||||
assert.Equal(t, tc.ExpectedCompletion, comp, name)
|
||||
|
||||
expectedDirective := cobra.ShellCompDirectiveNoFileComp
|
||||
if tc.ExpectedDirective != 0 {
|
||||
expectedDirective = tc.ExpectedDirective
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedDirective, directive, name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCompletion(t *testing.T) {
|
||||
comp, directive := noCompletion(nil, nil, "any")
|
||||
assert.Nil(t, comp)
|
||||
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
|
||||
}
|
||||
243
server/cmd/mmctl/commands/compliance_export.go
Normal file
243
server/cmd/mmctl/commands/compliance_export.go
Normal file
@@ -0,0 +1,243 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ComplianceExportCmd = &cobra.Command{
|
||||
Use: "compliance-export",
|
||||
Short: "Management of compliance exports",
|
||||
}
|
||||
|
||||
var ComplianceExportListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List compliance export jobs, sorted by creation date descending (newest first)",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(complianceExportListCmdF),
|
||||
}
|
||||
|
||||
var ComplianceExportShowCmd = &cobra.Command{
|
||||
Use: "show [complianceExportJobID]",
|
||||
Example: "compliance-export show o98rj3ur83dp5dppfyk5yk6osy",
|
||||
Short: "Show compliance export job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(complianceExportShowCmdF),
|
||||
}
|
||||
|
||||
var ComplianceExportCancelCmd = &cobra.Command{
|
||||
Use: "cancel [complianceExportJobID]",
|
||||
Example: "compliance-export cancel o98rj3ur83dp5dppfyk5yk6osy",
|
||||
Short: "Cancel compliance export job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(complianceExportCancelCmdF),
|
||||
}
|
||||
|
||||
var ComplianceExportDownloadCmd = &cobra.Command{
|
||||
Use: "download [complianceExportJobID] [output filepath (optional)]",
|
||||
Example: "compliance-export download o98rj3ur83dp5dppfyk5yk6osy",
|
||||
Short: "Download compliance export file",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: withClient(complianceExportDownloadCmdF),
|
||||
}
|
||||
|
||||
var ComplianceExportCreateCmd = &cobra.Command{
|
||||
Use: "create [complianceExportType] --date \"2025-03-27 -0400\"",
|
||||
Example: "compliance-export create csv --date \"2025-03-27 -0400\"",
|
||||
Long: "Create a compliance export job, of type 'csv' or 'actiance' or 'globalrelay'. If --date is set, the job will run for one day, from 12am to 12am (minus one millisecond) inclusively, in the format with timezone offset: `\"YYYY-MM-DD -0000\"`. E.g., \"2024-10-21 -0400\" for Oct 21, 2024 EDT timezone. \"2023-11-01 +0000\" for Nov 01, 2024 UTC. If set, the 'start' and 'end' flags will be ignored.\n\n" +
|
||||
"Important: Running a compliance export job from mmctl will NOT affect the next scheduled job's batch_start_time. This means that if you run a compliance export job from mmctl, the next scheduled job will run from the batch_end_time of the previous scheduled job, as usual.",
|
||||
Short: "Create a compliance export job, of type 'csv' or 'actiance' or 'globalrelay'",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: withClient(complianceExportCreateCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ComplianceExportListCmd.Flags().Int("page", 0, "Page number to fetch for the list of compliance export jobs")
|
||||
ComplianceExportListCmd.Flags().Int("per-page", DefaultPageSize, "Number of compliance export jobs to be fetched")
|
||||
ComplianceExportListCmd.Flags().Bool("all", false, "Fetch all compliance export jobs. --page flag will be ignored if provided")
|
||||
|
||||
ComplianceExportDownloadCmd.Flags().Int("num-retries", 5, "Number of retries if the download fails")
|
||||
|
||||
ComplianceExportCreateCmd.Flags().String(
|
||||
"date",
|
||||
"",
|
||||
"Run the export for one day, from 12am to 12am (minus one millisecond) inclusively, in the format with timezone offset: `\"YYYY-MM-DD -0000\"`. E.g., `\"2024-10-21 -0400\"` for Oct 21, 2024 EDT timezone. `\"2023-11-01 +0000\"` for Nov 01, 2024 UTC. If set, the 'start' and 'end' flags will be ignored.",
|
||||
)
|
||||
ComplianceExportCreateCmd.Flags().Int(
|
||||
"start",
|
||||
0,
|
||||
"The start timestamp in unix milliseconds. Posts with updateAt >= start will be exported. If set, 'end' must be set as well. eg, `1743048000000` for 2025-03-27 EDT.",
|
||||
)
|
||||
ComplianceExportCreateCmd.Flags().Int(
|
||||
"end",
|
||||
0,
|
||||
"The end timestamp in unix milliseconds. Posts with updateAt <= end will be exported. If set, 'start' must be set as well. eg, `1743134400000` for 2025-03-28 EDT.",
|
||||
)
|
||||
|
||||
ComplianceExportCmd.AddCommand(
|
||||
ComplianceExportListCmd,
|
||||
ComplianceExportShowCmd,
|
||||
ComplianceExportCancelCmd,
|
||||
ComplianceExportDownloadCmd,
|
||||
ComplianceExportCreateCmd,
|
||||
)
|
||||
RootCmd.AddCommand(ComplianceExportCmd)
|
||||
}
|
||||
|
||||
func complianceExportListCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
return jobListCmdF(c, command, "message_export", "")
|
||||
}
|
||||
|
||||
func complianceExportShowCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
job, _, err := c.GetJob(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get compliance export job: %w", err)
|
||||
}
|
||||
|
||||
printJob(job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func complianceExportCancelCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
if _, err := c.CancelJob(context.TODO(), args[0]); err != nil {
|
||||
return fmt.Errorf("failed to cancel compliance export job: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func complianceExportDownloadCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
jobID := args[0]
|
||||
var path string
|
||||
if len(args) > 1 {
|
||||
path = args[1]
|
||||
} else {
|
||||
path = jobID + ".zip"
|
||||
}
|
||||
|
||||
retries, _ := command.Flags().GetInt("num-retries")
|
||||
|
||||
downloadFn := func(outFile *os.File) (string, error) {
|
||||
return c.DownloadComplianceExport(context.TODO(), jobID, outFile)
|
||||
}
|
||||
|
||||
suggestedFilename, err := downloadFile(path, downloadFn, retries, "compliance export")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we didn't provide a path and got a suggested filename, rename the file
|
||||
if len(args) == 1 && suggestedFilename != "" && suggestedFilename != path {
|
||||
// If the suggested name already exists, don't overwrite
|
||||
if _, err := os.Stat(suggestedFilename); err == nil {
|
||||
printer.PrintWarning(fmt.Sprintf("File with the server's suggested name %q already exists, keeping %q", suggestedFilename, path))
|
||||
} else {
|
||||
if err := os.Rename(path, suggestedFilename); err != nil {
|
||||
printer.PrintWarning(fmt.Sprintf("Could not rename file to the server's suggested name %q: %v", suggestedFilename, err))
|
||||
} else {
|
||||
path = suggestedFilename
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("Compliance export file downloaded to %q", path))
|
||||
return nil
|
||||
}
|
||||
|
||||
func complianceExportCreateCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
exportType := args[0]
|
||||
if exportType != model.ComplianceExportTypeActiance &&
|
||||
exportType != model.ComplianceExportTypeCsv &&
|
||||
exportType != model.ComplianceExportTypeGlobalrelay {
|
||||
return fmt.Errorf("invalid export type: %s, must be one of: csv, actiance, globalrelay", exportType)
|
||||
}
|
||||
|
||||
dateStr, err := command.Flags().GetString("date")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
start, err := command.Flags().GetInt("start")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
end, err := command.Flags().GetInt("end")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
startTimestamp, endTimestamp, err := getStartAndEnd(dateStr, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
startTime := strconv.FormatInt(startTimestamp, 10)
|
||||
endTime := strconv.FormatInt(endTimestamp, 10)
|
||||
exportDir := path.Join(model.ComplianceExportPath, fmt.Sprintf("%s-%s-%s", time.Now().Format(model.ComplianceExportDirectoryFormat), startTime, endTime))
|
||||
|
||||
// If start and end are 0, we need to not set those keys in the job data.
|
||||
// This will make the job like a manual job (it will pick up where the previous job left off).
|
||||
data := model.StringMap{
|
||||
shared.JobDataInitiatedBy: "mmctl",
|
||||
shared.JobDataExportType: exportType,
|
||||
shared.JobDataBatchStartId: "",
|
||||
shared.JobDataJobStartId: "",
|
||||
}
|
||||
if startTimestamp != 0 && endTimestamp != 0 {
|
||||
data[shared.JobDataBatchStartTime] = startTime
|
||||
data[shared.JobDataJobStartTime] = startTime
|
||||
data[shared.JobDataJobEndTime] = endTime
|
||||
data[shared.JobDataExportDir] = exportDir
|
||||
}
|
||||
|
||||
job := &model.Job{
|
||||
Type: model.JobTypeMessageExport,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if job, _, err = c.CreateJob(context.TODO(), job); err != nil {
|
||||
return fmt.Errorf("failed to create compliance export job: %w", err)
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("Compliance export job created with ID: %s", job.Id))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getStartAndEnd returns the start and end timestamps in unix milliseconds
|
||||
func getStartAndEnd(dateStr string, start int, end int) (int64, int64, error) {
|
||||
if dateStr == "" && start == 0 && end == 0 {
|
||||
// return 0 so that the job will be like a manual job
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
if dateStr != "" && (start > 0 || end > 0) {
|
||||
return 0, 0, errors.New("if date is used, start and end must not be set")
|
||||
}
|
||||
|
||||
if dateStr != "" {
|
||||
t, err := time.Parse("2006-01-02 -0700", dateStr)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("could not parse date string: %s, use the format with time zone offset: YYYY-MM-DD -0700, eg for EDT: `2024-12-24 -0400`, error details: %w", dateStr, err)
|
||||
}
|
||||
endTimestamp := t.AddDate(0, 0, 1).UnixMilli() - 1
|
||||
return t.UnixMilli(), endTimestamp, nil
|
||||
}
|
||||
if start <= 0 || end <= 0 || start >= end {
|
||||
return 0, 0, fmt.Errorf("if date is not used, start: %d and end: %d must both be > 0, and start must be < end", start, end)
|
||||
}
|
||||
return int64(start), int64(end), nil
|
||||
}
|
||||
744
server/cmd/mmctl/commands/compliance_export_e2e_test.go
Normal file
744
server/cmd/mmctl/commands/compliance_export_e2e_test.go
Normal file
@@ -0,0 +1,744 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8"
|
||||
st "github.com/mattermost/mattermost/server/v8/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/message_export/shared"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestComplianceExportListCmdE2E() {
|
||||
s.SetupMessageExportTestHelper()
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportListCmdF(s.th.Client, cmd, nil)
|
||||
s.Require().EqualError(err, "failed to get jobs: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
jobType := model.JobTypeMessageExport
|
||||
|
||||
s.RunForSystemAdminAndLocal("List with no compliance export jobs", func(c client.Client) {
|
||||
// Ensure no jobs exist
|
||||
jobs, _, err := s.th.SystemAdminClient.GetJobsByType(context.Background(), jobType, 0, 1000)
|
||||
s.Require().NoError(err)
|
||||
|
||||
for _, job := range jobs {
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}
|
||||
|
||||
cmd := makeCmd()
|
||||
// Test default pagination
|
||||
printer.Clean()
|
||||
err = complianceExportListCmdF(c, cmd, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal("No jobs found", printer.GetLines()[0])
|
||||
|
||||
// Test with 1 per page
|
||||
printer.Clean()
|
||||
cmd = makeCmd()
|
||||
_ = cmd.Flags().Set("page", "0")
|
||||
_ = cmd.Flags().Set("per-page", "1")
|
||||
err = complianceExportListCmdF(c, cmd, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal("No jobs found", printer.GetLines()[0])
|
||||
|
||||
// Test with all items
|
||||
printer.Clean()
|
||||
cmd = makeCmd()
|
||||
_ = cmd.Flags().Set("all", "true")
|
||||
err = complianceExportListCmdF(c, cmd, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("List compliance export jobs", func(c client.Client) {
|
||||
now := model.GetMillis()
|
||||
// Create 2 jobs
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusSuccess,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
job2, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 100,
|
||||
Status: model.JobStatusSuccess,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 100,
|
||||
LastActivityAt: now - 100,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure jobs are deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job2.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
// Test default pagination
|
||||
printer.Clean()
|
||||
cmd := makeCmd()
|
||||
err = complianceExportListCmdF(c, cmd, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(job2.Id, printer.GetLines()[0].(*model.Job).Id)
|
||||
s.Require().Equal(job.Id, printer.GetLines()[1].(*model.Job).Id)
|
||||
|
||||
// Test with 1 per page
|
||||
printer.Clean()
|
||||
cmd = makeCmd()
|
||||
_ = cmd.Flags().Set("page", "0")
|
||||
_ = cmd.Flags().Set("per-page", "1")
|
||||
err = complianceExportListCmdF(c, cmd, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(job2.Id, printer.GetLines()[0].(*model.Job).Id)
|
||||
|
||||
// Test with all items
|
||||
printer.Clean()
|
||||
cmd = makeCmd()
|
||||
_ = cmd.Flags().Set("all", "true")
|
||||
err = complianceExportListCmdF(c, cmd, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(job2.Id, printer.GetLines()[0].(*model.Job).Id)
|
||||
s.Require().Equal(job.Id, printer.GetLines()[1].(*model.Job).Id)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestComplianceExportShowCmdE2E() {
|
||||
s.SetupMessageExportTestHelper()
|
||||
|
||||
now := model.GetMillis()
|
||||
|
||||
// Create a job
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusSuccess,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure job is deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportShowCmdF(s.th.Client, cmd, []string{job.Id})
|
||||
s.Require().EqualError(err, "failed to get compliance export job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Show non-existent job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportShowCmdF(c, cmd, []string{"non-existent-job-id"})
|
||||
s.Require().EqualError(err, "failed to get compliance export job: Sorry, we could not find the page., There doesn't appear to be an api call for the url='/api/v4/jobs/non-existent-job-id'. Typo? are you missing a team_id or user_id as part of the url?")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Show existing job", func(c client.Client) {
|
||||
now := model.GetMillis()
|
||||
// Create a job
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusSuccess,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure job is deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
printer.Clean()
|
||||
cmd := makeCmd()
|
||||
err = complianceExportShowCmdF(c, cmd, []string{job.Id})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(job.Id, printer.GetLines()[0].(*model.Job).Id)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestComplianceExportCancelCmdE2E() {
|
||||
s.SetupMessageExportTestHelper()
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
now := model.GetMillis()
|
||||
// Create a job
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusInProgress,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure job is deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
cmd := makeCmd()
|
||||
err = complianceExportCancelCmdF(s.th.Client, cmd, []string{job.Id})
|
||||
s.Require().EqualError(err, "failed to cancel compliance export job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Cancel non-existent job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportCancelCmdF(c, cmd, []string{"non-existent-job-id"})
|
||||
s.Require().EqualError(err, "failed to cancel compliance export job: Sorry, we could not find the page., There doesn't appear to be an api call for the url='/api/v4/jobs/non-existent-job-id/cancel'. Typo? are you missing a team_id or user_id as part of the url?")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Cancel existing job", func(c client.Client) {
|
||||
now := model.GetMillis()
|
||||
// Create a job
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusInProgress,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure job is deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
printer.Clean()
|
||||
cmd := makeCmd()
|
||||
err = complianceExportCancelCmdF(c, cmd, []string{job.Id})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
// Verify job was cancelled
|
||||
job, _, err = s.th.SystemAdminClient.GetJob(context.Background(), job.Id)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(model.JobStatusCanceled, job.Status)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Error cancelling job in non-cancellable state", func(c client.Client) {
|
||||
now := model.GetMillis()
|
||||
// Create a job
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusInProgress,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
_, err = s.th.SystemAdminClient.UpdateJobStatus(context.Background(), job.Id, model.JobStatusCanceled, true)
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure job is deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
printer.Clean()
|
||||
cmd := makeCmd()
|
||||
err = complianceExportCancelCmdF(c, cmd, []string{job.Id})
|
||||
s.Require().EqualError(err, "failed to cancel compliance export job: Could not request cancellation for job that is not in a cancelable state.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestComplianceExportDownloadCmdE2E() {
|
||||
s.SetupMessageExportTestHelper()
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
now := model.GetMillis()
|
||||
// Create a job
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusSuccess,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure job is deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
cmd := makeCmd()
|
||||
cmd.Flags().Int("num-retries", 0, "")
|
||||
err = complianceExportDownloadCmdF(s.th.Client, cmd, []string{job.Id})
|
||||
s.Require().EqualError(err, "failed to download compliance export after 0 retries: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Download non-existent job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := makeCmd()
|
||||
cmd.Flags().Int("num-retries", 0, "")
|
||||
err := complianceExportDownloadCmdF(c, cmd, []string{"non-existent-job-id"})
|
||||
s.Require().EqualError(err, "failed to download compliance export after 0 retries: Sorry, we could not find the page., There doesn't appear to be an api call for the url='/api/v4/jobs/non-existent-job-id/download'. Typo? are you missing a team_id or user_id as part of the url?")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("existing, non empty compliance export", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "test.zip")
|
||||
f, err := os.Create(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
_, err = f.WriteString("test data")
|
||||
s.Require().NoError(err)
|
||||
err = f.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
defer func() {
|
||||
err = os.Remove(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
}()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("num-retries", 0, "")
|
||||
|
||||
err = complianceExportDownloadCmdF(c, cmd, []string{"jobId", importFilePath})
|
||||
s.Require().EqualError(err, "compliance export file already exists")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("download with explicit path", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
downloadPath := "explicit_path.zip"
|
||||
defer os.Remove(downloadPath)
|
||||
|
||||
serverDataDir, err := filepath.Abs(*s.th.App.Config().FileSettings.Directory)
|
||||
s.Require().NoError(err)
|
||||
|
||||
exportDir := "job_test_export"
|
||||
// Create a compliance export with two zip files
|
||||
exportFilePath := filepath.Join(serverDataDir, exportDir)
|
||||
err = os.Mkdir(exportFilePath, 0755)
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
err = os.RemoveAll(exportFilePath)
|
||||
s.Require().NoError(err)
|
||||
}()
|
||||
|
||||
// Create first zip file
|
||||
zipPath1 := exportFilePath + "/export1.zip"
|
||||
f1, err := os.Create(zipPath1)
|
||||
s.Require().NoError(err)
|
||||
_, err = f1.WriteString("test data 1")
|
||||
s.Require().NoError(err)
|
||||
err = f1.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Create second zip file
|
||||
zipPath2 := exportFilePath + "/export2.zip"
|
||||
f2, err := os.Create(zipPath2)
|
||||
s.Require().NoError(err)
|
||||
_, err = f2.WriteString("test data 2")
|
||||
s.Require().NoError(err)
|
||||
err = f2.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.T().Cleanup(func() {
|
||||
err = os.RemoveAll(exportFilePath)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
now := model.GetMillis()
|
||||
// Create a job
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusSuccess,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
Data: model.StringMap{"export_dir": exportDir, "is_downloadable": "true"},
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure job is deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
cmd := makeCmd()
|
||||
cmd.Flags().Int("num-retries", 0, "")
|
||||
|
||||
err = complianceExportDownloadCmdF(c, cmd, []string{job.Id, downloadPath})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Contains(printer.GetLines()[0], fmt.Sprintf("Compliance export file downloaded to %q", downloadPath))
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
// Verify the file was downloaded
|
||||
_, err = os.Stat(downloadPath)
|
||||
s.Require().NoError(err)
|
||||
defer os.Remove(downloadPath)
|
||||
|
||||
// Verify the file is a zip with a directory with two zip files
|
||||
zipReader, err := zip.OpenReader(downloadPath)
|
||||
s.Require().NoError(err)
|
||||
defer zipReader.Close()
|
||||
|
||||
// Check that we have the expected files in the zip
|
||||
foundExport1 := false
|
||||
foundExport2 := false
|
||||
for _, file := range zipReader.File {
|
||||
if file.Name == "export1.zip" {
|
||||
foundExport1 = true
|
||||
// Verify contents of export1.zip
|
||||
rc, err := file.Open()
|
||||
s.Require().NoError(err)
|
||||
content, err := io.ReadAll(rc)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal("test data 1", string(content))
|
||||
rc.Close()
|
||||
} else if file.Name == "export2.zip" {
|
||||
foundExport2 = true
|
||||
// Verify contents of export2.zip
|
||||
rc, err := file.Open()
|
||||
s.Require().NoError(err)
|
||||
content, err := io.ReadAll(rc)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal("test data 2", string(content))
|
||||
rc.Close()
|
||||
} else {
|
||||
s.Failf("unexpected file found in downloaded zip", file.Name)
|
||||
}
|
||||
}
|
||||
s.Require().True(foundExport1, "export1.zip not found in downloaded file")
|
||||
s.Require().True(foundExport2, "export2.zip not found in downloaded file")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("download with explicit path", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
serverDataDir, err := filepath.Abs(*s.th.App.Config().FileSettings.Directory)
|
||||
s.Require().NoError(err)
|
||||
|
||||
exportDir := "job_test_export"
|
||||
expectedDownloadPath := exportDir + ".zip"
|
||||
// Create a compliance export with two zip files
|
||||
exportFilePath := filepath.Join(serverDataDir, exportDir)
|
||||
err = os.Mkdir(exportFilePath, 0755)
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
err = os.RemoveAll(exportFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// also remove the downloaded file
|
||||
err = os.Remove(expectedDownloadPath)
|
||||
s.Require().NoError(err)
|
||||
}()
|
||||
|
||||
// Create first zip file
|
||||
zipPath1 := exportFilePath + "/export1.zip"
|
||||
f1, err := os.Create(zipPath1)
|
||||
s.Require().NoError(err)
|
||||
_, err = f1.WriteString("test data 1")
|
||||
s.Require().NoError(err)
|
||||
err = f1.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Create second zip file
|
||||
zipPath2 := exportFilePath + "/export2.zip"
|
||||
f2, err := os.Create(zipPath2)
|
||||
s.Require().NoError(err)
|
||||
_, err = f2.WriteString("test data 2")
|
||||
s.Require().NoError(err)
|
||||
err = f2.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
defer func() {
|
||||
err = os.RemoveAll(exportFilePath)
|
||||
s.Require().NoError(err)
|
||||
}()
|
||||
|
||||
now := model.GetMillis()
|
||||
// Create a job
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(), &model.Job{
|
||||
Id: st.NewTestID(),
|
||||
CreateAt: now - 1000,
|
||||
Status: model.JobStatusSuccess,
|
||||
Type: model.JobTypeMessageExport,
|
||||
StartAt: now - 1000,
|
||||
LastActivityAt: now - 1000,
|
||||
Data: model.StringMap{"export_dir": exportDir, "is_downloadable": "true"},
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
// Ensure job is deleted from the database
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}()
|
||||
|
||||
cmd := makeCmd()
|
||||
cmd.Flags().Int("num-retries", 0, "")
|
||||
|
||||
err = complianceExportDownloadCmdF(c, cmd, []string{job.Id})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Contains(printer.GetLines()[0], fmt.Sprintf("Compliance export file downloaded to %q", expectedDownloadPath))
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
// Verify the file was downloaded
|
||||
_, err = os.Stat(expectedDownloadPath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Verify the file is a zip with a directory with two zip files
|
||||
zipReader, err := zip.OpenReader(expectedDownloadPath)
|
||||
s.Require().NoError(err)
|
||||
defer zipReader.Close()
|
||||
|
||||
// Check that we have the expected files in the zip
|
||||
foundExport1 := false
|
||||
foundExport2 := false
|
||||
for _, file := range zipReader.File {
|
||||
if file.Name == "export1.zip" {
|
||||
foundExport1 = true
|
||||
// Verify contents of export1.zip
|
||||
rc, err := file.Open()
|
||||
s.Require().NoError(err)
|
||||
content, err := io.ReadAll(rc)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal("test data 1", string(content))
|
||||
rc.Close()
|
||||
} else if file.Name == "export2.zip" {
|
||||
foundExport2 = true
|
||||
// Verify contents of export2.zip
|
||||
rc, err := file.Open()
|
||||
s.Require().NoError(err)
|
||||
content, err := io.ReadAll(rc)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal("test data 2", string(content))
|
||||
rc.Close()
|
||||
} else {
|
||||
s.Failf("unexpected file found in downloaded zip", file.Name)
|
||||
}
|
||||
}
|
||||
s.Require().True(foundExport1, "export1.zip not found in downloaded file")
|
||||
s.Require().True(foundExport2, "export2.zip not found in downloaded file")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestComplianceExportMmctlJobStartTimeE2E() {
|
||||
s.SetupMessageExportTestHelper()
|
||||
|
||||
s.RunForSystemAdminAndLocal("mmctl job uses batch_start_time from previous regular job", func(c client.Client) {
|
||||
// Ensure no jobs exist before we start
|
||||
jobs, _, err := s.th.SystemAdminClient.GetJobsByType(context.Background(), model.JobTypeMessageExport, 0, 1000)
|
||||
s.Require().NoError(err)
|
||||
for _, job := range jobs {
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}
|
||||
|
||||
now := model.GetMillis()
|
||||
|
||||
// Create a regular (non-mmctl) export job
|
||||
regularStartTime := now - 10000
|
||||
regularEndTime := now - 5000
|
||||
regularJob := s.runJobForTest(map[string]string{
|
||||
shared.JobDataBatchStartTime: strconv.FormatInt(regularStartTime, 10),
|
||||
shared.JobDataJobEndTime: strconv.FormatInt(regularEndTime, 10),
|
||||
})
|
||||
|
||||
s.Require().Equal(model.JobStatusSuccess, regularJob.Status, "Regular job should complete successfully")
|
||||
s.Require().NotEmpty(regularJob.Data[shared.JobDataBatchStartTime], "Regular job should have a batch start time")
|
||||
regularJobBatchStartTime := regularJob.Data[shared.JobDataBatchStartTime]
|
||||
|
||||
// Run an mmctl-initiated export job
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("date", "", "")
|
||||
cmd.Flags().Int("start", 0, "")
|
||||
cmd.Flags().Int("end", 0, "")
|
||||
err = complianceExportCreateCmdF(c, cmd, []string{model.ComplianceExportTypeActiance})
|
||||
s.Require().NoError(err, "Should create mmctl job successfully")
|
||||
|
||||
// Find the mmctl job
|
||||
jobs, _, err = s.th.SystemAdminClient.GetJobsByType(context.Background(), model.JobTypeMessageExport, 0, 10)
|
||||
s.Require().NoError(err)
|
||||
s.Require().True(len(jobs) > 1, "Should have at least 2 jobs")
|
||||
|
||||
// The most recent job should be the mmctl job
|
||||
mmctlJob := jobs[0]
|
||||
s.Require().Equal("mmctl", mmctlJob.Data[shared.JobDataInitiatedBy])
|
||||
|
||||
// Wait for the mmctl job to complete
|
||||
s.checkJobForStatus(mmctlJob.Id, model.JobStatusSuccess)
|
||||
mmctlJob = s.getMostRecentJobWithId(mmctlJob.Id)
|
||||
|
||||
// The job_start_time should match the batch_start_time from the previous regular job
|
||||
s.Require().Equal(regularJobBatchStartTime, mmctlJob.Data[shared.JobDataJobStartTime],
|
||||
"mmctl job should use batch_start_time from previous regular job as its job_start_time")
|
||||
|
||||
// Clean up jobs
|
||||
for _, job := range jobs {
|
||||
result, err := s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("mmctl job ignores previous mmctl jobs and uses regular job", func(c client.Client) {
|
||||
// Ensure no jobs exist before we start
|
||||
jobs, _, err := s.th.SystemAdminClient.GetJobsByType(context.Background(), model.JobTypeMessageExport, 0, 1000)
|
||||
s.Require().NoError(err)
|
||||
for _, job := range jobs {
|
||||
var result string
|
||||
result, err = s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}
|
||||
|
||||
now := model.GetMillis()
|
||||
|
||||
// Create a regular (non-mmctl) export job
|
||||
regularStartTime := now - 10000
|
||||
regularEndTime := now - 5000
|
||||
regularJob := s.runJobForTest(map[string]string{
|
||||
shared.JobDataBatchStartTime: strconv.FormatInt(regularStartTime, 10),
|
||||
shared.JobDataJobEndTime: strconv.FormatInt(regularEndTime, 10),
|
||||
})
|
||||
|
||||
s.Require().Equal(model.JobStatusSuccess, regularJob.Status, "Regular job should complete successfully")
|
||||
s.Require().NotEmpty(regularJob.Data[shared.JobDataBatchStartTime], "Regular job should have a batch start time")
|
||||
regularJobBatchStartTime := regularJob.Data[shared.JobDataBatchStartTime]
|
||||
|
||||
// Run an mmctl-initiated export job with an explicit start time (different from the regular job)
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("date", "", "")
|
||||
cmd.Flags().Int("start", int(now-2000), "")
|
||||
cmd.Flags().Int("end", int(now-1000), "")
|
||||
err = complianceExportCreateCmdF(c, cmd, []string{model.ComplianceExportTypeActiance})
|
||||
s.Require().NoError(err, "Should create first mmctl job successfully")
|
||||
|
||||
// Find the mmctl job
|
||||
jobs, _, err = s.th.SystemAdminClient.GetJobsByType(context.Background(), model.JobTypeMessageExport, 0, 10)
|
||||
s.Require().NoError(err)
|
||||
s.Require().True(len(jobs) > 1, "Should have at least 2 jobs")
|
||||
|
||||
// The most recent job should be the mmctl job
|
||||
mmctlJob1 := jobs[0]
|
||||
s.Require().Equal("mmctl", mmctlJob1.Data[shared.JobDataInitiatedBy])
|
||||
|
||||
// Wait for the mmctl job to complete
|
||||
s.checkJobForStatus(mmctlJob1.Id, model.JobStatusSuccess)
|
||||
mmctlJob1 = s.getMostRecentJobWithId(mmctlJob1.Id)
|
||||
|
||||
// Verify this job has a different batch_start_time than the regular job
|
||||
s.Require().NotEqual(regularJobBatchStartTime, mmctlJob1.Data[shared.JobDataBatchStartTime],
|
||||
"First mmctl job should have a different batch_start_time than regular job")
|
||||
|
||||
// Run a second mmctl-initiated export job WITHOUT a specified start time
|
||||
cmd = &cobra.Command{}
|
||||
cmd.Flags().String("date", "", "")
|
||||
cmd.Flags().Int("start", 0, "")
|
||||
cmd.Flags().Int("end", 0, "")
|
||||
err = complianceExportCreateCmdF(c, cmd, []string{model.ComplianceExportTypeActiance})
|
||||
s.Require().NoError(err, "Should create second mmctl job successfully")
|
||||
|
||||
// Find the second mmctl job
|
||||
jobs, _, err = s.th.SystemAdminClient.GetJobsByType(context.Background(), model.JobTypeMessageExport, 0, 10)
|
||||
s.Require().NoError(err)
|
||||
s.Require().True(len(jobs) > 2, "Should have at least 3 jobs")
|
||||
|
||||
// The most recent job should be the second mmctl job
|
||||
mmctlJob2 := jobs[0]
|
||||
s.Require().Equal("mmctl", mmctlJob2.Data[shared.JobDataInitiatedBy])
|
||||
|
||||
// Wait for the second mmctl job to complete
|
||||
s.checkJobForStatus(mmctlJob2.Id, model.JobStatusSuccess)
|
||||
mmctlJob2 = s.getMostRecentJobWithId(mmctlJob2.Id)
|
||||
|
||||
// The job_start_time of the second mmctl job should match the batch_start_time from the regular job,
|
||||
// not from the mmctl job that ran in between
|
||||
s.Require().Equal(regularJobBatchStartTime, mmctlJob2.Data[shared.JobDataJobStartTime],
|
||||
"Second mmctl job should use batch_start_time from previous regular job as its job_start_time, not from previous mmctl job")
|
||||
s.Require().NotEqual(mmctlJob1.Data[shared.JobDataBatchStartTime], mmctlJob2.Data[shared.JobDataJobStartTime],
|
||||
"Second mmctl job should not use batch_start_time from previous mmctl job as its job_start_time")
|
||||
|
||||
// Clean up jobs
|
||||
for _, job := range jobs {
|
||||
result, err := s.th.App.Srv().Store().Job().Delete(job.Id)
|
||||
s.Require().NoError(err, "Failed to delete job (result: %v)", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
430
server/cmd/mmctl/commands/compliance_export_test.go
Normal file
430
server/cmd/mmctl/commands/compliance_export_test.go
Normal file
@@ -0,0 +1,430 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestComplianceExportListCmdF() {
|
||||
s.Run("list default pagination", func() {
|
||||
s.SetupTest() // Reset mocks before test
|
||||
printer.Clean()
|
||||
var mockJobs []*model.Job
|
||||
|
||||
// Test with default pagination
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "message_export", "", 0, DefaultPageSize).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportListCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
|
||||
// Test with 10 per page
|
||||
printer.Clean()
|
||||
cmd = makeCmd()
|
||||
_ = cmd.Flags().Set("per-page", "10")
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "message_export", "", 0, 10).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = complianceExportListCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
|
||||
// Test with all items
|
||||
printer.Clean()
|
||||
cmd = makeCmd()
|
||||
_ = cmd.Flags().Set("all", "true")
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "message_export", "", 0, DefaultPageSize).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = complianceExportListCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("list with paging", func() {
|
||||
// Create 5 mock jobs
|
||||
mockJobs := make([]*model.Job, 5)
|
||||
for i := range 5 {
|
||||
mockJobs[i] = &model.Job{
|
||||
Id: model.NewId(),
|
||||
CreateAt: model.GetMillis() - int64(i*1000),
|
||||
}
|
||||
}
|
||||
|
||||
// Test paging with 2 jobs per page
|
||||
printer.Clean()
|
||||
cmd := makeCmd()
|
||||
_ = cmd.Flags().Set("all", "true")
|
||||
_ = cmd.Flags().Set("per-page", "2")
|
||||
|
||||
// Expect 4 API calls (2 jobs each for first 2 pages, 1 job for last page, then a call with 0 jobs)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "message_export", "", 0, 2).
|
||||
Return(mockJobs[0:2], &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "message_export", "", 1, 2).
|
||||
Return(mockJobs[2:4], &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "message_export", "", 2, 2).
|
||||
Return(mockJobs[4:5], &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "message_export", "", 3, 2).
|
||||
Return(mockJobs[5:], &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := complianceExportListCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 5)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// Verify jobs are printed in correct order
|
||||
for i := range 5 {
|
||||
s.Equal(mockJobs[i].Id, printer.GetLines()[i].(*model.Job).Id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestComplianceExportShowCmdF() {
|
||||
s.Run("show job successfully", func() {
|
||||
s.SetupTest() // Reset mocks before test
|
||||
printer.Clean()
|
||||
mockJob := &model.Job{
|
||||
Id: model.NewId(),
|
||||
CreateAt: model.GetMillis(),
|
||||
Type: model.JobTypeMessageExport,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJob(context.TODO(), mockJob.Id).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportShowCmdF(s.client, cmd, []string{mockJob.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
|
||||
s.Run("show job with error", func() {
|
||||
s.SetupTest() // Reset mocks before test
|
||||
printer.Clean()
|
||||
mockError := &model.AppError{
|
||||
Message: "failed to get job",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJob(context.TODO(), "invalid-job-id").
|
||||
Return(nil, &model.Response{}, mockError).
|
||||
Times(1)
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportShowCmdF(s.client, cmd, []string{"invalid-job-id"})
|
||||
s.Require().NotNil(err)
|
||||
s.EqualError(err, "failed to get compliance export job: failed to get job")
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestComplianceExportCancelCmdF() {
|
||||
s.Run("cancel job successfully", func() {
|
||||
s.SetupTest() // Reset mocks before test
|
||||
printer.Clean()
|
||||
id := model.NewId()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CancelJob(context.TODO(), id).
|
||||
Return(&model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportCancelCmdF(s.client, cmd, []string{id})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("cancel job with get error", func() {
|
||||
s.SetupTest() // Reset mocks before test
|
||||
printer.Clean()
|
||||
mockError := &model.AppError{
|
||||
Message: "failed to get job",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CancelJob(context.TODO(), "invalid-job-id").
|
||||
Return(&model.Response{}, mockError).
|
||||
Times(1)
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportCancelCmdF(s.client, cmd, []string{"invalid-job-id"})
|
||||
s.Require().NotNil(err)
|
||||
s.EqualError(err, "failed to cancel compliance export job: failed to get job")
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("cancel job with cancel error", func() {
|
||||
s.SetupTest() // Reset mocks before test
|
||||
printer.Clean()
|
||||
id := model.NewId()
|
||||
|
||||
mockError := &model.AppError{
|
||||
Message: "failed to cancel job",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CancelJob(context.TODO(), id).
|
||||
Return(&model.Response{}, mockError).
|
||||
Times(1)
|
||||
|
||||
cmd := makeCmd()
|
||||
err := complianceExportCancelCmdF(s.client, cmd, []string{id})
|
||||
s.Require().NotNil(err)
|
||||
s.EqualError(err, "failed to cancel compliance export job: failed to cancel job")
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestComplianceExportDownloadCmdF() {
|
||||
mockJob := &model.Job{
|
||||
Id: model.NewId(),
|
||||
CreateAt: model.GetMillis(),
|
||||
Type: model.JobTypeMessageExport,
|
||||
}
|
||||
|
||||
s.Run("download job file successfully", func() {
|
||||
printer.Clean()
|
||||
s.T().Cleanup(func() {
|
||||
err := os.Remove("suggested-filename.zip")
|
||||
s.NoError(err)
|
||||
})
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DownloadComplianceExport(gomock.Any(), mockJob.Id, gomock.Any()).
|
||||
Return("suggested-filename.zip", nil).
|
||||
Times(1)
|
||||
|
||||
cmd := makeCmd()
|
||||
cmd.Flags().Int("num-retries", 5, "")
|
||||
err := complianceExportDownloadCmdF(s.client, cmd, []string{mockJob.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal(fmt.Sprintf("Compliance export file downloaded to %q", "suggested-filename.zip"), printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("download job file with explicit path", func() {
|
||||
printer.Clean()
|
||||
s.T().Cleanup(func() {
|
||||
err := os.Remove("custom-path.zip")
|
||||
s.NoError(err)
|
||||
})
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DownloadComplianceExport(context.TODO(), mockJob.Id, gomock.Any()).
|
||||
Return("", nil).
|
||||
Times(1)
|
||||
|
||||
cmd := makeCmd()
|
||||
cmd.Flags().Int("num-retries", 5, "")
|
||||
err := complianceExportDownloadCmdF(s.client, cmd, []string{mockJob.Id, "custom-path.zip"})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal(fmt.Sprintf("Compliance export file downloaded to %q", "custom-path.zip"), printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("download job with error", func() {
|
||||
printer.Clean()
|
||||
mockError := &model.AppError{
|
||||
Message: "failed to download file",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DownloadComplianceExport(context.TODO(), mockJob.Id, gomock.Any()).
|
||||
Return("", mockError).
|
||||
Times(6) // Initial attempt + 5 retries
|
||||
|
||||
cmd := makeCmd()
|
||||
cmd.Flags().Int("num-retries", 5, "")
|
||||
err := complianceExportDownloadCmdF(s.client, cmd, []string{mockJob.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.EqualError(err, "failed to download compliance export after 5 retries: failed to download file")
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// Ensure output file does not exist
|
||||
_, err = os.Stat(mockJob.Id + "BAR.zip")
|
||||
s.Error(err)
|
||||
s.True(os.IsNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetStartAndEnd(t *testing.T) {
|
||||
type args struct {
|
||||
dateStr string
|
||||
start int
|
||||
end int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStart int64
|
||||
expectedEnd int64
|
||||
wantErr bool
|
||||
}{
|
||||
// check with: https://www.epochconverter.com/
|
||||
{
|
||||
name: "parse a date in EDT (-0400)",
|
||||
args: args{
|
||||
dateStr: "2024-10-21 -0400",
|
||||
},
|
||||
expectedStart: 1729483200000,
|
||||
expectedEnd: 1729569599999,
|
||||
},
|
||||
{
|
||||
name: "parse a date in UTC (+0)",
|
||||
args: args{
|
||||
dateStr: "2024-10-21 +0000",
|
||||
},
|
||||
expectedStart: 1729468800000,
|
||||
expectedEnd: 1729555199999,
|
||||
},
|
||||
{
|
||||
name: "parse a date in CDT (-0500)",
|
||||
args: args{
|
||||
dateStr: "2024-10-21 -0500",
|
||||
},
|
||||
expectedStart: 1729486800000,
|
||||
expectedEnd: 1729573199999,
|
||||
},
|
||||
{
|
||||
name: "bad format",
|
||||
args: args{
|
||||
dateStr: "2024-10-21 CT",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad format",
|
||||
args: args{
|
||||
dateStr: "2024-1-2 CDT",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "it's ok to not have date, start, or end",
|
||||
args: args{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "needs both start and end pt1",
|
||||
args: args{
|
||||
start: 12345,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "needs both start and end pt2",
|
||||
args: args{
|
||||
end: 12345,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "start and end",
|
||||
args: args{
|
||||
start: 12345,
|
||||
end: 678912,
|
||||
},
|
||||
expectedStart: 12345,
|
||||
expectedEnd: 678912,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "date and start",
|
||||
args: args{
|
||||
dateStr: "2024-10-21 -0400",
|
||||
start: 12345,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "date and end",
|
||||
args: args{
|
||||
dateStr: "2024-10-21 -0400",
|
||||
end: 678912,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotStart, gotEnd, err := getStartAndEnd(tt.args.dateStr, tt.args.start, tt.args.end)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getStartAndEnd() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotStart != tt.expectedStart {
|
||||
t.Errorf("getStartAndEnd() got = %v, want %v", gotStart, tt.expectedStart)
|
||||
}
|
||||
if gotEnd != tt.expectedEnd {
|
||||
t.Errorf("getStartAndEnd() got1 = %v, want %v", gotEnd, tt.expectedEnd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func makeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", DefaultPageSize, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
return cmd
|
||||
}
|
||||
613
server/cmd/mmctl/commands/config.go
Normal file
613
server/cmd/mmctl/commands/config.go
Normal file
@@ -0,0 +1,613 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const defaultEditor = "vi"
|
||||
|
||||
var ErrConfigInvalidPath = errors.New("selected path object is not valid")
|
||||
|
||||
var ConfigCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Configuration",
|
||||
}
|
||||
|
||||
var ConfigGetCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get config setting",
|
||||
Long: "Gets the value of a config setting by its name in dot notation.",
|
||||
Example: `config get SqlSettings.DriverName`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(configGetCmdF),
|
||||
}
|
||||
|
||||
var ConfigSetCmd = &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Set config setting",
|
||||
Long: "Sets the value of a config setting by its name in dot notation. Accepts multiple values for array settings",
|
||||
Example: "config set SqlSettings.DriverName postgres\nconfig set SqlSettings.DataSourceReplicas \"replica1\" \"replica2\"",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: withClient(configSetCmdF),
|
||||
}
|
||||
|
||||
var ConfigPatchCmd = &cobra.Command{
|
||||
Use: "patch <config-file>",
|
||||
Short: "Patch the config",
|
||||
Long: "Patches config settings with the given config file.",
|
||||
Example: "config patch /path/to/config.json",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(configPatchCmdF),
|
||||
}
|
||||
|
||||
var ConfigEditCmd = &cobra.Command{
|
||||
Use: "edit",
|
||||
Short: "Edit the config",
|
||||
Long: "Opens the editor defined in the EDITOR environment variable to modify the server's configuration and then uploads it",
|
||||
Example: "config edit",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(configEditCmdF),
|
||||
}
|
||||
|
||||
var ConfigResetCmd = &cobra.Command{
|
||||
Use: "reset",
|
||||
Short: "Reset config setting",
|
||||
Long: "Resets the value of a config setting by its name in dot notation or a setting section. Accepts multiple values for array settings.",
|
||||
Example: "config reset SqlSettings.DriverName LogSettings",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: withClient(configResetCmdF),
|
||||
}
|
||||
|
||||
var ConfigShowCmd = &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Writes the server configuration to STDOUT",
|
||||
Long: "Prints the server configuration and writes to STDOUT in JSON format.",
|
||||
Example: "config show",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(configShowCmdF),
|
||||
}
|
||||
|
||||
var ConfigReloadCmd = &cobra.Command{
|
||||
Use: "reload",
|
||||
Short: "Reload the server configuration",
|
||||
Long: "Reload the server configuration in case you want to new settings to be applied.",
|
||||
Example: "config reload",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(configReloadCmdF),
|
||||
}
|
||||
|
||||
var ConfigMigrateCmd = &cobra.Command{
|
||||
Use: "migrate [from_config] [to_config]",
|
||||
Short: "Migrate existing config between backends",
|
||||
Long: "Migrate a file-based configuration to (or from) a database-based configuration. Point the Mattermost server at the target configuration to start using it. Note that this command is only available in `--local` mode.",
|
||||
Example: `config migrate path/to/config.json "postgres://mmuser:mostest_password@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10"`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: withClient(configMigrateCmdF),
|
||||
}
|
||||
|
||||
var ConfigSubpathCmd = &cobra.Command{
|
||||
Use: "subpath",
|
||||
Short: "Update client asset loading to use the configured subpath",
|
||||
Long: "Update the hard-coded production client asset paths to take into account Mattermost running on a subpath. This command needs access to the Mattermost assets directory to be able to rewrite the paths.",
|
||||
Example: ` # you can rewrite the assets to use a subpath
|
||||
mmctl config subpath --assets-dir /opt/mattermost/client --path /mattermost
|
||||
|
||||
# the subpath can have multiple steps
|
||||
mmctl config subpath --assets-dir /opt/mattermost/client --path /my/custom/subpath
|
||||
|
||||
# or you can fallback to the root path passing /
|
||||
mmctl config subpath --assets-dir /opt/mattermost/client --path /`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: configSubpathCmdF,
|
||||
}
|
||||
|
||||
var ConfigExportCmd = &cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Export the server configuration",
|
||||
Long: "Export the server configuration in case you want to import somewhere else.",
|
||||
Example: "config export --remove-masked --remove-defaults",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(configExportCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ConfigResetCmd.Flags().Bool("confirm", false, "confirm you really want to reset all configuration settings to its default value")
|
||||
|
||||
ConfigSubpathCmd.Flags().StringP("assets-dir", "a", "", "directory of the Mattermost assets in the local filesystem")
|
||||
_ = ConfigSubpathCmd.MarkFlagRequired("assets-dir")
|
||||
ConfigSubpathCmd.Flags().StringP("path", "p", "", "path to update the assets with")
|
||||
_ = ConfigSubpathCmd.MarkFlagRequired("path")
|
||||
|
||||
ConfigExportCmd.Flags().Bool("remove-masked", true, "remove masked values from the exported configuration")
|
||||
ConfigExportCmd.Flags().Bool("remove-defaults", false, "remove default values from the exported configuration")
|
||||
|
||||
ConfigCmd.AddCommand(
|
||||
ConfigGetCmd,
|
||||
ConfigSetCmd,
|
||||
ConfigPatchCmd,
|
||||
ConfigEditCmd,
|
||||
ConfigResetCmd,
|
||||
ConfigShowCmd,
|
||||
ConfigReloadCmd,
|
||||
ConfigMigrateCmd,
|
||||
ConfigSubpathCmd,
|
||||
ConfigExportCmd,
|
||||
)
|
||||
RootCmd.AddCommand(ConfigCmd)
|
||||
}
|
||||
|
||||
func getValue(path []string, obj any) (any, bool) {
|
||||
r := reflect.ValueOf(obj)
|
||||
var val reflect.Value
|
||||
if r.Kind() == reflect.Map {
|
||||
val = r.MapIndex(reflect.ValueOf(path[0]))
|
||||
if val.IsValid() {
|
||||
val = val.Elem()
|
||||
}
|
||||
} else {
|
||||
val = r.FieldByName(path[0])
|
||||
}
|
||||
|
||||
if !val.IsValid() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(path) == 1:
|
||||
return val.Interface(), true
|
||||
case val.Kind() == reflect.Struct:
|
||||
return getValue(path[1:], val.Interface())
|
||||
case val.Kind() == reflect.Map:
|
||||
remainingPath := strings.Join(path[1:], ".")
|
||||
mapIter := val.MapRange()
|
||||
for mapIter.Next() {
|
||||
key := mapIter.Key().String()
|
||||
if strings.HasPrefix(remainingPath, key) {
|
||||
i := strings.Count(key, ".") + 2 // number of dots + a dot on each side
|
||||
mapVal := mapIter.Value()
|
||||
// if no sub field path specified, return the object
|
||||
if len(path[i:]) == 0 {
|
||||
return mapVal.Interface(), true
|
||||
}
|
||||
data := mapVal.Interface()
|
||||
if mapVal.Kind() == reflect.Pointer {
|
||||
data = mapVal.Elem().Interface() // if value is a pointer, dereference it
|
||||
}
|
||||
// pass subpath
|
||||
return getValue(path[i:], data)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func setValueWithConversion(val reflect.Value, newValue any) error {
|
||||
switch val.Kind() {
|
||||
case reflect.Struct:
|
||||
val.Set(reflect.ValueOf(newValue))
|
||||
return nil
|
||||
case reflect.Slice:
|
||||
v := reflect.ValueOf(newValue)
|
||||
if v.Kind() != reflect.Slice {
|
||||
// Special case when setting a string to a byte slice
|
||||
if val.Type().Elem().Kind() == reflect.Uint8 && v.Kind() == reflect.String {
|
||||
s := newValue.(string)
|
||||
v = reflect.ValueOf([]byte(s))
|
||||
} else {
|
||||
return errors.Errorf("target value is of type %v and provided value is %v", val.Kind(), v.Kind())
|
||||
}
|
||||
}
|
||||
val.Set(v)
|
||||
return nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
bits := val.Type().Bits()
|
||||
v, err := strconv.ParseInt(newValue.(string), 10, bits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("target value is of type %v and provided value is not, err: %w", val.Kind(), err)
|
||||
}
|
||||
val.SetInt(v)
|
||||
return nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
bits := val.Type().Bits()
|
||||
v, err := strconv.ParseFloat(newValue.(string), bits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("target value is of type %v and provided value is not, err: %w", val.Kind(), err)
|
||||
}
|
||||
val.SetFloat(v)
|
||||
return nil
|
||||
case reflect.String:
|
||||
val.SetString(newValue.(string))
|
||||
return nil
|
||||
case reflect.Bool:
|
||||
v, err := strconv.ParseBool(newValue.(string))
|
||||
if err != nil {
|
||||
return fmt.Errorf("target value is of type %v and provided value is not, err: %w", val.Kind(), err)
|
||||
}
|
||||
val.SetBool(v)
|
||||
return nil
|
||||
default:
|
||||
return errors.Errorf("value type %v is not supported", val.Kind())
|
||||
}
|
||||
}
|
||||
|
||||
func setValue(path []string, obj reflect.Value, newValue any) error {
|
||||
var val reflect.Value
|
||||
switch obj.Kind() {
|
||||
case reflect.Struct:
|
||||
val = obj.FieldByName(path[0])
|
||||
case reflect.Map:
|
||||
val = obj.MapIndex(reflect.ValueOf(path[0]))
|
||||
if val.IsValid() {
|
||||
val = val.Elem()
|
||||
}
|
||||
default:
|
||||
val = obj
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Invalid {
|
||||
return ErrConfigInvalidPath
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
if val.Kind() == reflect.Pointer {
|
||||
return setValue(path, val.Elem(), newValue)
|
||||
} else if obj.Kind() == reflect.Map {
|
||||
// since we cannot set map elements directly, we clone the value, set it, and then put it back in the map
|
||||
mapKey := reflect.ValueOf(path[0])
|
||||
subVal := obj.MapIndex(mapKey)
|
||||
if subVal.IsValid() {
|
||||
tmpVal := reflect.New(subVal.Elem().Type())
|
||||
if err := setValueWithConversion(tmpVal.Elem(), newValue); err != nil {
|
||||
return err
|
||||
}
|
||||
obj.SetMapIndex(mapKey, tmpVal)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return setValueWithConversion(val, newValue)
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Struct {
|
||||
return setValue(path[1:], val, newValue)
|
||||
} else if val.Kind() == reflect.Map {
|
||||
remainingPath := strings.Join(path[1:], ".")
|
||||
mapIter := val.MapRange()
|
||||
for mapIter.Next() {
|
||||
key := mapIter.Key().String()
|
||||
if strings.HasPrefix(remainingPath, key) {
|
||||
mapVal := mapIter.Value()
|
||||
|
||||
if mapVal.Kind() == reflect.Pointer {
|
||||
mapVal = mapVal.Elem() // if value is a pointer, dereference it
|
||||
}
|
||||
i := len(strings.Split(key, ".")) + 1
|
||||
|
||||
if i > len(path)-1 { // leaf element
|
||||
i = 1
|
||||
mapVal = val
|
||||
}
|
||||
// pass subpath
|
||||
return setValue(path[i:], mapVal, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.New("path object type is not supported")
|
||||
}
|
||||
|
||||
func setConfigValue(path []string, config *model.Config, newValue []string) error {
|
||||
if len(newValue) > 1 {
|
||||
return setValue(path, reflect.ValueOf(config).Elem(), newValue)
|
||||
}
|
||||
return setValue(path, reflect.ValueOf(config).Elem(), newValue[0])
|
||||
}
|
||||
|
||||
func resetConfigValue(path []string, config *model.Config, newValue any) error {
|
||||
nv := reflect.ValueOf(newValue)
|
||||
if nv.Kind() == reflect.Pointer {
|
||||
switch nv.Elem().Kind() {
|
||||
case reflect.Int:
|
||||
return setValue(path, reflect.ValueOf(config).Elem(), strconv.Itoa(*newValue.(*int)))
|
||||
case reflect.Bool:
|
||||
return setValue(path, reflect.ValueOf(config).Elem(), strconv.FormatBool(*newValue.(*bool)))
|
||||
default:
|
||||
return setValue(path, reflect.ValueOf(config).Elem(), *newValue.(*string))
|
||||
}
|
||||
} else {
|
||||
return setValue(path, reflect.ValueOf(config).Elem(), newValue)
|
||||
}
|
||||
}
|
||||
|
||||
func configGetCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
printer.SetFormat(printer.FormatJSON)
|
||||
|
||||
config, _, err := c.GetConfig(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := strings.Split(args[0], ".")
|
||||
val, ok := getValue(path, *config)
|
||||
if !ok {
|
||||
return errors.New("invalid key")
|
||||
}
|
||||
|
||||
if cloudRestricted(config, path) && reflect.ValueOf(val).IsNil() {
|
||||
return fmt.Errorf("accessing this config path: %s is restricted in a cloud environment", args[0])
|
||||
}
|
||||
|
||||
printer.Print(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
func configSetCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
||||
config, _, err := c.GetConfig(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := parseConfigPath(args[0])
|
||||
if cErr := setConfigValue(path, config, args[1:]); cErr != nil {
|
||||
if errors.Is(cErr, ErrConfigInvalidPath) && cloudRestricted(config, path) {
|
||||
return fmt.Errorf("changing this config path: %s is restricted in a cloud environment", args[0])
|
||||
}
|
||||
|
||||
return cErr
|
||||
}
|
||||
newConfig, _, err := c.PatchConfig(context.TODO(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.PrintT("Value changed successfully", newConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func configPatchCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
||||
configBytes, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, _, err := c.GetConfig(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get original plugin map
|
||||
var pluginConfig map[string]map[string]any
|
||||
if config.PluginSettings.Plugins != nil {
|
||||
pluginConfig = (config.Clone()).PluginSettings.Plugins
|
||||
}
|
||||
|
||||
// apply path onto the existing config
|
||||
if jErr := json.Unmarshal(configBytes, config); jErr != nil {
|
||||
return jErr
|
||||
}
|
||||
|
||||
// merge config plugin map on top of the original, and assign the
|
||||
// result to the config key
|
||||
config.PluginSettings.Plugins = MergePluginConfigs(pluginConfig, config.PluginSettings.Plugins)
|
||||
|
||||
newConfig, _, err := c.PatchConfig(context.TODO(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.PrintT("Config patched successfully", newConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func configEditCmdF(c client.Client, _ *cobra.Command, _ []string) error {
|
||||
config, _, err := c.GetConfig(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configBytes, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.CreateTemp(os.TempDir(), "mmctl-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
file.Close()
|
||||
os.Remove(file.Name())
|
||||
}()
|
||||
if _, writeErr := file.Write(configBytes); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
|
||||
editor := os.Getenv("EDITOR")
|
||||
if editor == "" {
|
||||
editor = defaultEditor
|
||||
}
|
||||
|
||||
editorCmd := exec.Command(editor, file.Name())
|
||||
editorCmd.Stdout = os.Stdout
|
||||
editorCmd.Stdin = os.Stdin
|
||||
editorCmd.Stderr = os.Stderr
|
||||
|
||||
if cmdErr := editorCmd.Run(); cmdErr != nil {
|
||||
return cmdErr
|
||||
}
|
||||
|
||||
newConfigBytes, err := os.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if jErr := json.Unmarshal(newConfigBytes, config); jErr != nil {
|
||||
return jErr
|
||||
}
|
||||
|
||||
newConfig, _, err := c.UpdateConfig(context.TODO(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.PrintT("Config updated successfully", newConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func configResetCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
confirmFlag, _ := cmd.Flags().GetBool("confirm")
|
||||
|
||||
if !confirmFlag && len(args) > 0 {
|
||||
if err := getConfirmation(fmt.Sprintf(
|
||||
"Are you sure you want to reset %s to their default value? (YES/NO): ",
|
||||
args[0]), false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig := &model.Config{}
|
||||
defaultConfig.SetDefaults()
|
||||
config, _, err := c.GetConfig(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
path := parseConfigPath(arg)
|
||||
defaultValue, ok := getValue(path, *defaultConfig)
|
||||
if !ok {
|
||||
return errors.New("invalid key")
|
||||
}
|
||||
nErr := resetConfigValue(path, config, defaultValue)
|
||||
if nErr != nil {
|
||||
return nErr
|
||||
}
|
||||
}
|
||||
newConfig, _, err := c.UpdateConfig(context.TODO(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.PrintT("Value/s reset successfully", newConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func configShowCmdF(c client.Client, _ *cobra.Command, _ []string) error {
|
||||
printer.SetSingle(true)
|
||||
printer.SetFormat(printer.FormatJSON)
|
||||
config, _, err := c.GetConfig(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Print(config)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseConfigPath(configPath string) []string {
|
||||
return strings.Split(configPath, ".")
|
||||
}
|
||||
|
||||
func configReloadCmdF(c client.Client, _ *cobra.Command, _ []string) error {
|
||||
_, err := c.ReloadConfig(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configMigrateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
isLocal, _ := cmd.Flags().GetBool("local")
|
||||
if !isLocal {
|
||||
return errors.New("this command is only available in local mode. Please set the --local flag")
|
||||
}
|
||||
|
||||
_, err := c.MigrateConfig(context.TODO(), args[0], args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configSubpathCmdF(cmd *cobra.Command, _ []string) error {
|
||||
assetsDir, _ := cmd.Flags().GetString("assets-dir")
|
||||
path, _ := cmd.Flags().GetString("path")
|
||||
|
||||
if err := utils.UpdateAssetsSubpathInDir(path, assetsDir); err != nil {
|
||||
return errors.Wrap(err, "failed to update assets subpath")
|
||||
}
|
||||
|
||||
printer.Print("Config subpath successfully modified")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloudRestricted(cfg any, path []string) bool {
|
||||
return cloudRestrictedR(reflect.TypeOf(cfg), path)
|
||||
}
|
||||
|
||||
// cloudRestricted checks if the config path is restricted to the cloud
|
||||
func cloudRestrictedR(t reflect.Type, path []string) bool {
|
||||
if t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
|
||||
for field := range t.Fields() {
|
||||
if len(path) == 0 || field.Name != path[0] {
|
||||
continue
|
||||
}
|
||||
|
||||
accessTag := field.Tag.Get(model.ConfigAccessTagType)
|
||||
if strings.Contains(accessTag, model.ConfigAccessTagCloudRestrictable) {
|
||||
return true
|
||||
}
|
||||
|
||||
return cloudRestrictedR(field.Type, path[1:])
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func configExportCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
||||
removeDefaults, _ := cmd.Flags().GetBool("remove-defaults")
|
||||
removeMasked, _ := cmd.Flags().GetBool("remove-masked")
|
||||
config, _, err := c.GetConfigWithOptions(context.TODO(), model.GetConfigOptions{
|
||||
RemoveDefaults: removeDefaults,
|
||||
RemoveMasked: removeMasked,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.SetSingle(true)
|
||||
printer.SetFormat(printer.FormatJSON)
|
||||
|
||||
printer.Print(config)
|
||||
|
||||
return nil
|
||||
}
|
||||
432
server/cmd/mmctl/commands/config_e2e_test.go
Normal file
432
server/cmd/mmctl/commands/config_e2e_test.go
Normal file
@@ -0,0 +1,432 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigResetCmdE2E() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("System admin and local reset", func(c client.Client) {
|
||||
printer.Clean()
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowEmailAddress = false })
|
||||
resetCmd := &cobra.Command{}
|
||||
resetCmd.Flags().Bool("confirm", true, "")
|
||||
err := configResetCmdF(c, resetCmd, []string{"PrivacySettings"})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
config := s.th.App.Config()
|
||||
s.Require().True(*config.PrivacySettings.ShowEmailAddress)
|
||||
})
|
||||
|
||||
s.Run("Reset for user without permission", func() {
|
||||
printer.Clean()
|
||||
resetCmd := &cobra.Command{}
|
||||
args := []string{"PrivacySettings"}
|
||||
resetCmd.Flags().Bool("confirm", true, "")
|
||||
err := configResetCmdF(s.th.Client, resetCmd, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Assert().Errorf(err, "You do not have the appropriate permissions.")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigPatchCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
tmpFile, err := os.CreateTemp(os.TempDir(), "config_*.json")
|
||||
s.Require().Nil(err)
|
||||
|
||||
invalidFile, err := os.CreateTemp(os.TempDir(), "invalid_config_*.json")
|
||||
s.Require().Nil(err)
|
||||
|
||||
_, err = tmpFile.WriteString(configFilePayload)
|
||||
s.Require().Nil(err)
|
||||
|
||||
defer func() {
|
||||
os.Remove(tmpFile.Name())
|
||||
os.Remove(invalidFile.Name())
|
||||
}()
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T4051 - System admin and local patch", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := configPatchCmdF(c, &cobra.Command{}, []string{tmpFile.Name()})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T4052 - System admin and local patch with invalid file", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := configPatchCmdF(c, &cobra.Command{}, []string{invalidFile.Name()})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("MM-T4053 - Patch config for user without permission", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := configPatchCmdF(s.th.Client, &cobra.Command{}, []string{tmpFile.Name()})
|
||||
s.Require().NotNil(err)
|
||||
s.Assert().Errorf(err, "You do not have the appropriate permissions.")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigGetCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
var driver string
|
||||
if d := s.th.App.Config().SqlSettings.DriverName; d != nil {
|
||||
driver = *d
|
||||
}
|
||||
|
||||
s.RunForSystemAdminAndLocal("Get config value for a given key", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
args := []string{"SqlSettings.DriverName"}
|
||||
err := configGetCmdF(c, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(driver, *(printer.GetLines()[0].(*string)))
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Expect error when using a nonexistent key", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
args := []string{"NonExistent.Key"}
|
||||
err := configGetCmdF(c, &cobra.Command{}, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Get config value for a given key without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
args := []string{"SqlSettings.DriverName"}
|
||||
err := configGetCmdF(s.th.Client, &cobra.Command{}, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigSetCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("Set config value for a given key", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
args := []string{"SqlSettings.DriverName", "postgres"}
|
||||
err := configSetCmdF(c, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
config, ok := printer.GetLines()[0].(*model.Config)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal("postgres", *(config.SqlSettings.DriverName))
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Get error if the key doesn't exist", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
args := []string{"SqlSettings.WrongKey", "postgres"}
|
||||
err := configSetCmdF(c, &cobra.Command{}, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Set config value for a given key without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
args := []string{"SqlSettings.DriverName", "postgres"}
|
||||
err := configSetCmdF(s.th.Client, &cobra.Command{}, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("ImportSettings.Directory cannot be set via API but can via local mode", func() {
|
||||
printer.Clean()
|
||||
originalDir := *s.th.App.Config().ImportSettings.Directory
|
||||
|
||||
args := []string{"ImportSettings.Directory", "./api-blocked-import"}
|
||||
err := configSetCmdF(s.th.SystemAdminClient, &cobra.Command{}, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "not allowed due to security reasons")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
|
||||
// Verify value didn't change
|
||||
s.Require().Equal(originalDir, *s.th.App.Config().ImportSettings.Directory)
|
||||
|
||||
printer.Clean()
|
||||
args = []string{"ImportSettings.Directory", "./local-allowed-import"}
|
||||
err = configSetCmdF(s.th.LocalClient, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
config, ok := printer.GetLines()[0].(*model.Config)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal("./local-allowed-import", *config.ImportSettings.Directory)
|
||||
|
||||
// Reset to original
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ImportSettings.Directory = &originalDir
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigEditCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("Edit a key in config", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
// ensure the value before editing
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableSVGs = false })
|
||||
|
||||
// create a shell script to edit config
|
||||
content := `#!/bin/bash
|
||||
sed -i'old' 's/\"EnableSVGs\": false/\"EnableSVGs\": true/' $1
|
||||
rm $1'old'`
|
||||
|
||||
file, err := os.CreateTemp(os.TempDir(), "config_edit_*.sh")
|
||||
s.Require().Nil(err)
|
||||
defer func() {
|
||||
os.Remove(file.Name())
|
||||
}()
|
||||
_, err = file.WriteString(content)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Nil(file.Close())
|
||||
s.Require().Nil(os.Chmod(file.Name(), 0700))
|
||||
|
||||
s.T().Setenv("EDITOR", file.Name())
|
||||
|
||||
// check the value after editing
|
||||
err = configEditCmdF(c, nil, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
config := s.th.App.Config()
|
||||
s.Require().True(*config.ServiceSettings.EnableSVGs)
|
||||
})
|
||||
|
||||
s.Run("Edit config value without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := configEditCmdF(s.th.Client, nil, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Error(err, "You do not have the appropriate permissions.")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigShowCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("Show server configs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := configShowCmdF(c, nil, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Show server configs without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := configShowCmdF(s.th.Client, nil, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Error(err, "You do not have the appropriate permissions")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigReloadCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("Reload server config", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := configReloadCmdF(c, nil, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Reload server config without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := configReloadCmdF(s.th.Client, nil, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Error(err, "You do not have the appropriate permissions")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigExportCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("Get config normally", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := configExportCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
m, ok := printer.GetLines()[0].(map[string]any)
|
||||
s.Require().True(ok)
|
||||
if c == s.th.LocalClient {
|
||||
// filter config is used to convert the config to a map[string]any
|
||||
// local client has unrestricted access to the config
|
||||
expectedConfig, err2 := model.FilterConfig(s.th.App.Config(), model.ConfigFilterOptions{GetConfigOptions: model.GetConfigOptions{}})
|
||||
s.Require().NoError(err2)
|
||||
s.Require().Equal(expectedConfig, m)
|
||||
} else {
|
||||
// filter config is used to convert the config to a map[string]any
|
||||
// system admin client has restricted access to the config
|
||||
expectedConfig, err2 := model.FilterConfig(s.th.App.GetSanitizedConfig(), model.ConfigFilterOptions{GetConfigOptions: model.GetConfigOptions{}})
|
||||
s.Require().NoError(err2)
|
||||
s.Require().Equal(expectedConfig, m)
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("Should remove masked values for system admin client", func() {
|
||||
printer.Clean()
|
||||
|
||||
exportCmd := &cobra.Command{}
|
||||
exportCmd.Flags().Bool("remove-masked", true, "")
|
||||
err := configExportCmdF(s.th.SystemAdminClient, exportCmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
m, ok := printer.GetLines()[0].(map[string]any)
|
||||
s.Require().True(ok)
|
||||
ss, ok := m["SqlSettings"].(map[string]any)
|
||||
s.Require().True(ok)
|
||||
_, ok = ss["DataSource"]
|
||||
s.Require().False(ok)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Should retrieve configuration as-is with local client", func() {
|
||||
printer.Clean()
|
||||
|
||||
exportCmd := &cobra.Command{}
|
||||
err := configExportCmdF(s.th.LocalClient, exportCmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
m, ok := printer.GetLines()[0].(map[string]any)
|
||||
s.Require().True(ok)
|
||||
ss, ok := m["SqlSettings"].(map[string]any)
|
||||
s.Require().True(ok)
|
||||
ds, ok := ss["DataSource"]
|
||||
s.Require().True(ok)
|
||||
cfg := s.th.App.Config()
|
||||
s.Require().Equal(*cfg.SqlSettings.DataSource, ds)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Should remove default values", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
exportCmd := &cobra.Command{}
|
||||
exportCmd.Flags().Bool("remove-defaults", true, "")
|
||||
err := configExportCmdF(c, exportCmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
m, ok := printer.GetLines()[0].(map[string]any)
|
||||
s.Require().True(ok)
|
||||
ss, ok := m["TeamSettings"].(map[string]any)
|
||||
s.Require().True(ok)
|
||||
_, ok = ss["MaxUsersPerTeam"] // it's not being changed by the test suite
|
||||
s.Require().False(ok)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Get config value for a given key without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := configExportCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestConfigMigrateCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("Should fail without the --local flag", func() {
|
||||
printer.Clean()
|
||||
args := []string{"config.json", "output.json"}
|
||||
|
||||
err := configMigrateCmdF(s.th.Client, &cobra.Command{}, args)
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal("this command is only available in local mode. Please set the --local flag", err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Should be able to migrate config to file", func() {
|
||||
printer.Clean()
|
||||
args := []string{"config.json", "output.json"}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("local", true, "")
|
||||
|
||||
err := configMigrateCmdF(s.th.LocalClient, cmd, args)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Should be able to migrate config to database", func() {
|
||||
printer.Clean()
|
||||
|
||||
// Get the current database DSN from the test configuration
|
||||
currentDSN := *s.th.App.Config().SqlSettings.DataSource
|
||||
args := []string{"config.json", currentDSN}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("local", true, "")
|
||||
|
||||
err := configMigrateCmdF(s.th.LocalClient, cmd, args)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Should fail on error when migrating config", func() {
|
||||
printer.Clean()
|
||||
args := []string{"from", "to"}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("local", true, "")
|
||||
|
||||
err := configMigrateCmdF(s.th.LocalClient, cmd, args)
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal("Failed to migrate config store.", err.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
1027
server/cmd/mmctl/commands/config_test.go
Normal file
1027
server/cmd/mmctl/commands/config_test.go
Normal file
File diff suppressed because it is too large
Load Diff
48
server/cmd/mmctl/commands/docs.go
Normal file
48
server/cmd/mmctl/commands/docs.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
var DocsCmd = &cobra.Command{
|
||||
Use: "docs",
|
||||
Short: "Generates mmctl documentation",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: docsCmdF,
|
||||
}
|
||||
|
||||
func init() {
|
||||
DocsCmd.Flags().StringP("directory", "d", "docs", "The directory where the docs would be generated in.")
|
||||
|
||||
RootCmd.AddCommand(DocsCmd)
|
||||
}
|
||||
|
||||
func docsCmdF(cmd *cobra.Command, args []string) error {
|
||||
outDir, _ := cmd.Flags().GetString("directory")
|
||||
fileInfo, err := os.Stat(outDir)
|
||||
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if createErr := os.Mkdir(outDir, 0755); createErr != nil {
|
||||
return createErr
|
||||
}
|
||||
} else if !fileInfo.IsDir() {
|
||||
return fmt.Errorf("File %q is not a directory", outDir)
|
||||
}
|
||||
|
||||
err = doc.GenReSTTree(RootCmd, outDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
27
server/cmd/mmctl/commands/enterprise.go
Normal file
27
server/cmd/mmctl/commands/enterprise.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build enterprise
|
||||
|
||||
// This file is needed to ensure the enterprise code get complied
|
||||
// when running tests. See https://mattermost.atlassian.net/browse/MM-54929
|
||||
// for more details.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
// Enterprise Deps
|
||||
_ "github.com/elastic/go-elasticsearch/v8"
|
||||
_ "github.com/gorilla/handlers"
|
||||
_ "github.com/hako/durafmt"
|
||||
_ "github.com/hashicorp/memberlist"
|
||||
_ "github.com/mattermost/gosaml2"
|
||||
_ "github.com/mattermost/ldap"
|
||||
_ "github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
||||
_ "github.com/mattermost/mattermost/server/v8/enterprise"
|
||||
_ "github.com/mattermost/rsc/qr"
|
||||
_ "github.com/prometheus/client_golang/prometheus"
|
||||
_ "github.com/prometheus/client_golang/prometheus/collectors"
|
||||
_ "github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
_ "github.com/tylerb/graceful"
|
||||
)
|
||||
52
server/cmd/mmctl/commands/errors.go
Normal file
52
server/cmd/mmctl/commands/errors.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// ErrEntityNotFound is thrown when an entity (user, team, etc.)
|
||||
// is not found, returning the id sent by arguments
|
||||
type ErrEntityNotFound struct {
|
||||
Type string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (e ErrEntityNotFound) Error() string {
|
||||
return fmt.Sprintf("%s %s not found", e.Type, e.ID)
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
type BadRequestError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *BadRequestError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
// ExtractErrorFromResponse extracts the error from the response,
|
||||
// encapsulating it if matches the common cases, such as when it's
|
||||
// not found, and when we've made a bad request
|
||||
func ExtractErrorFromResponse(r *model.Response, err error) error {
|
||||
switch r.StatusCode {
|
||||
case http.StatusNotFound:
|
||||
return &NotFoundError{Msg: err.Error()}
|
||||
case http.StatusBadRequest:
|
||||
return &BadRequestError{Msg: err.Error()}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
318
server/cmd/mmctl/commands/export.go
Normal file
318
server/cmd/mmctl/commands/export.go
Normal file
@@ -0,0 +1,318 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ExportCmd = &cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Management of exports",
|
||||
}
|
||||
|
||||
var ExportCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create export file",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(exportCreateCmdF),
|
||||
}
|
||||
|
||||
var ExportDownloadCmd = &cobra.Command{
|
||||
Use: "download [exportname] [filepath]",
|
||||
Short: "Download export files",
|
||||
Example: ` # you can indicate the name of the export and its destination path
|
||||
$ mmctl export download samplename sample_export.zip
|
||||
|
||||
# or if you only indicate the name, the path would match it
|
||||
$ mmctl export download sample_export.zip`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: withClient(exportDownloadCmdF),
|
||||
}
|
||||
|
||||
var ExportGeneratePresignedURLCmd = &cobra.Command{
|
||||
Use: "generate-presigned-url [exportname]",
|
||||
Short: "Generate a presigned url for an export file. This is helpful when an export is big and might have trouble downloading from the Mattermost server.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(exportGeneratePresignedURLCmdF),
|
||||
}
|
||||
|
||||
var ExportDeleteCmd = &cobra.Command{
|
||||
Use: "delete [exportname]",
|
||||
Aliases: []string{"rm"},
|
||||
Example: " export delete export_file.zip",
|
||||
Short: "Delete export file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(exportDeleteCmdF),
|
||||
}
|
||||
|
||||
var ExportListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List export files",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(exportListCmdF),
|
||||
}
|
||||
|
||||
var ExportJobCmd = &cobra.Command{
|
||||
Use: "job",
|
||||
Short: "List, show and cancel export jobs",
|
||||
}
|
||||
|
||||
var ExportJobListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Example: " export job list",
|
||||
Short: "List export jobs",
|
||||
Aliases: []string{"ls"},
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(exportJobListCmdF),
|
||||
}
|
||||
|
||||
var ExportJobShowCmd = &cobra.Command{
|
||||
Use: "show [exportJobID]",
|
||||
Example: " export job show o98rj3ur83dp5dppfyk5yk6osy",
|
||||
Short: "Show export job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(exportJobShowCmdF),
|
||||
}
|
||||
|
||||
var ExportJobCancelCmd = &cobra.Command{
|
||||
Use: "cancel [exportJobID]",
|
||||
Example: " export job cancel o98rj3ur83dp5dppfyk5yk6osy",
|
||||
Short: "Cancel export job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(exportJobCancelCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ExportCreateCmd.Flags().Bool("no-attachments", false, "Exclude file attachments from the export file.")
|
||||
ExportCreateCmd.Flags().Bool("include-archived-channels", false, "Include archived channels in the export file.")
|
||||
ExportCreateCmd.Flags().Bool("include-profile-pictures", false, "Include profile pictures in the export file.")
|
||||
ExportCreateCmd.Flags().Bool("no-roles-and-schemes", false, "Exclude roles and custom permission schemes from the export file.")
|
||||
|
||||
ExportDownloadCmd.Flags().Int("num-retries", 5, "Number of retries to do to resume a download.")
|
||||
|
||||
ExportJobListCmd.Flags().Int("page", 0, "Page number to fetch for the list of export jobs")
|
||||
ExportJobListCmd.Flags().Int("per-page", DefaultPageSize, "Number of export jobs to be fetched")
|
||||
ExportJobListCmd.Flags().Bool("all", false, "Fetch all export jobs. --page flag will be ignore if provided")
|
||||
|
||||
ExportJobCmd.AddCommand(
|
||||
ExportJobListCmd,
|
||||
ExportJobShowCmd,
|
||||
ExportJobCancelCmd,
|
||||
)
|
||||
ExportCmd.AddCommand(
|
||||
ExportCreateCmd,
|
||||
ExportListCmd,
|
||||
ExportDeleteCmd,
|
||||
ExportDownloadCmd,
|
||||
ExportGeneratePresignedURLCmd,
|
||||
ExportJobCmd,
|
||||
)
|
||||
RootCmd.AddCommand(ExportCmd)
|
||||
}
|
||||
|
||||
func exportCreateCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
data := make(map[string]string)
|
||||
|
||||
excludeAttachments, _ := command.Flags().GetBool("no-attachments")
|
||||
if !excludeAttachments {
|
||||
data["include_attachments"] = "true"
|
||||
}
|
||||
|
||||
excludeRolesAndSchemes, _ := command.Flags().GetBool("no-roles-and-schemes")
|
||||
if !excludeRolesAndSchemes {
|
||||
data["include_roles_and_schemes"] = "true"
|
||||
}
|
||||
|
||||
includeArchivedChannels, _ := command.Flags().GetBool("include-archived-channels")
|
||||
if includeArchivedChannels {
|
||||
data["include_archived_channels"] = "true"
|
||||
}
|
||||
|
||||
includeProfilePictures, _ := command.Flags().GetBool("include-profile-pictures")
|
||||
if includeProfilePictures {
|
||||
data["include_profile_pictures"] = "true"
|
||||
}
|
||||
|
||||
job, _, err := c.CreateJob(context.TODO(), &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create export process job: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Export process job successfully created, ID: {{.Id}}", job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportListCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
exports, _, err := c.ListExports(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list exports: %w", err)
|
||||
}
|
||||
|
||||
if len(exports) == 0 {
|
||||
printer.Print("No export files found")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, name := range exports {
|
||||
printer.Print(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportDeleteCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
if _, err := c.DeleteExport(context.TODO(), name); err != nil {
|
||||
return fmt.Errorf("failed to delete export: %w", err)
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("Export file %q has been deleted", name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportGeneratePresignedURLCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
presignedURL, _, err := c.GeneratePresignedURL(context.TODO(), name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate export link: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Export link: {{.Link}}\nExpiration: {{.Expiration}}", map[string]any{
|
||||
"Link": presignedURL.URL,
|
||||
"Expiration": presignedURL.Expiration.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportDownloadCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
var path string
|
||||
name := args[0]
|
||||
if len(args) > 1 {
|
||||
path = args[1]
|
||||
}
|
||||
if path == "" {
|
||||
path = name
|
||||
}
|
||||
|
||||
retries, _ := command.Flags().GetInt("num-retries")
|
||||
|
||||
downloadFn := func(outFile *os.File) (string, error) {
|
||||
off, err := outFile.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to seek file: %w", err)
|
||||
}
|
||||
|
||||
_, _, err = c.DownloadExport(context.TODO(), name, outFile, off)
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err := downloadFile(path, downloadFn, retries, "export")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("Export file downloaded to %q", path))
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadFile handles the common logic for downloading files in export and compliance-export commands
|
||||
func downloadFile(path string, downloadFn func(*os.File) (string, error), retries int, fileType string) (string, error) {
|
||||
var outFile *os.File
|
||||
var createdFile bool
|
||||
info, err := os.Stat(path)
|
||||
switch {
|
||||
case err != nil && !os.IsNotExist(err):
|
||||
// some error occurred and not because file doesn't exist
|
||||
return "", fmt.Errorf("failed to stat %s file: %w", fileType, err)
|
||||
case err == nil && info.Size() > 0:
|
||||
// we exit to avoid overwriting an existing non-empty file
|
||||
return "", fmt.Errorf("%s file already exists", fileType)
|
||||
case err != nil:
|
||||
// file does not exist, we create it
|
||||
outFile, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
|
||||
createdFile = true
|
||||
default:
|
||||
// no error, file exists, we double check the permissions and then open it
|
||||
permErr := os.Chmod(path, 0600)
|
||||
if permErr != nil {
|
||||
return "", fmt.Errorf("failed to change permissions on output file: %w", permErr)
|
||||
}
|
||||
|
||||
outFile, err = os.OpenFile(path, os.O_WRONLY, 0600)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create/open %s file: %w", fileType, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
var suggestedFilename string
|
||||
for i := range retries + 1 { // need to include the first attempt
|
||||
suggestedFilename, err = downloadFn(outFile)
|
||||
if err != nil {
|
||||
if i >= retries {
|
||||
// Cleanup the file we created earlier
|
||||
if createdFile {
|
||||
rmErr := os.Remove(path)
|
||||
if rmErr != nil {
|
||||
printer.PrintError(fmt.Sprintf("Failed to cleanup tempory file: %s", rmErr))
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to download %s after %d retries: %w", fileType, retries, err)
|
||||
}
|
||||
printer.PrintWarning(fmt.Sprintf("Download attempt %d/%d failed. Retrying...", i+1, retries+1))
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return suggestedFilename, nil
|
||||
}
|
||||
|
||||
func exportJobListCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
return jobListCmdF(c, command, model.JobTypeExportProcess, "")
|
||||
}
|
||||
|
||||
func exportJobShowCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
job, _, err := c.GetJob(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get export job: %w", err)
|
||||
}
|
||||
|
||||
printJob(job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportJobCancelCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
||||
job, _, err := c.GetJob(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get export job: %w", err)
|
||||
}
|
||||
|
||||
if _, err := c.CancelJob(context.TODO(), job.Id); err != nil {
|
||||
return fmt.Errorf("failed to cancel export job: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
478
server/cmd/mmctl/commands/export_e2e_test.go
Normal file
478
server/cmd/mmctl/commands/export_e2e_test.go
Normal file
@@ -0,0 +1,478 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExportListCmdF() {
|
||||
s.SetupTestHelper()
|
||||
importName := "import_test.zip"
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "tests", importName)
|
||||
exportPath, err := filepath.Abs(filepath.Join(*s.th.App.Config().FileSettings.Directory,
|
||||
*s.th.App.Config().ExportSettings.Directory))
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Run("MM-T3914 - no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := exportListCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().EqualError(err, "failed to list exports: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3913 - no exports", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := exportListCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Equal("No export files found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3912 - some exports", func(c client.Client) {
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
numExports := 3
|
||||
for i := range numExports {
|
||||
exportName := fmt.Sprintf("export_%d.zip", i)
|
||||
err := utils.CopyFile(importFilePath, filepath.Join(exportPath, exportName))
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
|
||||
printer.Clean()
|
||||
|
||||
exports, appErr := s.th.App.ListExports()
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := exportListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), len(exports))
|
||||
for i, name := range printer.GetLines() {
|
||||
s.Require().Equal(exports[i], name.(string))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExportDeleteCmdF() {
|
||||
s.SetupTestHelper()
|
||||
importName := "import_test.zip"
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "tests", importName)
|
||||
exportPath, err := filepath.Abs(filepath.Join(*s.th.App.Config().FileSettings.Directory,
|
||||
*s.th.App.Config().ExportSettings.Directory))
|
||||
s.Require().Nil(err)
|
||||
|
||||
exportName := "export.zip"
|
||||
s.Run("MM-T3876 - no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := exportDeleteCmdF(s.th.Client, &cobra.Command{}, []string{exportName})
|
||||
s.Require().EqualError(err, "failed to delete export: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3843 - delete export", func(c client.Client) {
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := utils.CopyFile(importFilePath, filepath.Join(exportPath, exportName))
|
||||
s.Require().Nil(err)
|
||||
|
||||
printer.Clean()
|
||||
|
||||
exports, appErr := s.th.App.ListExports()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().NotEmpty(exports)
|
||||
s.Require().Equal(exportName, exports[0])
|
||||
|
||||
err = exportDeleteCmdF(c, cmd, []string{exportName})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Equal(fmt.Sprintf(`Export file "%s" has been deleted`, exportName), printer.GetLines()[0])
|
||||
|
||||
exports, appErr = s.th.App.ListExports()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Empty(exports)
|
||||
|
||||
printer.Clean()
|
||||
|
||||
// idempotence check
|
||||
err = exportDeleteCmdF(c, cmd, []string{exportName})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Equal(fmt.Sprintf(`Export file "%s" has been deleted`, exportName), printer.GetLines()[0])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExportCreateCmdF() {
|
||||
s.SetupTestHelper()
|
||||
|
||||
s.Run("MM-T3877 - no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := exportCreateCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().EqualError(err, "failed to create export process job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3839 - create export", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := exportCreateCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal("true", printer.GetLines()[0].(*model.Job).Data["include_attachments"])
|
||||
s.Require().Equal("true", printer.GetLines()[0].(*model.Job).Data["include_roles_and_schemes"])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3878 - create export without attachments", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
cmd.Flags().Bool("no-attachments", true, "")
|
||||
|
||||
err := exportCreateCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal("", printer.GetLines()[0].(*model.Job).Data["include_attachments"])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("create export without roles and schemes", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
cmd.Flags().Bool("no-roles-and-schemes", true, "")
|
||||
|
||||
err := exportCreateCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal("", printer.GetLines()[0].(*model.Job).Data["include_roles_and_schemes"])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExportDownloadCmdF() {
|
||||
s.SetupTestHelper()
|
||||
importName := "import_test.zip"
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "tests", importName)
|
||||
exportPath, err := filepath.Abs(filepath.Join(*s.th.App.Config().FileSettings.Directory,
|
||||
*s.th.App.Config().ExportSettings.Directory))
|
||||
s.Require().Nil(err)
|
||||
|
||||
exportName := "export.zip"
|
||||
|
||||
s.Run("MM-T3879 - no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("num-retries", 5, "")
|
||||
|
||||
err := exportDownloadCmdF(s.th.Client, cmd, []string{exportName})
|
||||
s.Require().EqualError(err, "failed to download export after 5 retries: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3880 - existing, non empty file", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("num-retries", 5, "")
|
||||
|
||||
downloadPath, err := filepath.Abs(exportName)
|
||||
s.Require().Nil(err)
|
||||
err = utils.CopyFile(importFilePath, downloadPath)
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(downloadPath)
|
||||
|
||||
err = exportDownloadCmdF(c, cmd, []string{exportName, downloadPath})
|
||||
s.Require().EqualError(err, "export file already exists")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3882 - export does not exist", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("num-retries", 5, "")
|
||||
|
||||
downloadPath, err := filepath.Abs(exportName)
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(downloadPath)
|
||||
|
||||
err = exportDownloadCmdF(c, cmd, []string{exportName, downloadPath})
|
||||
s.Require().EqualError(err, "failed to download export after 5 retries: Unable to find export file.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3883 - existing, empty file", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("num-retries", 5, "")
|
||||
|
||||
exportFilePath := filepath.Join(exportPath, exportName)
|
||||
err := utils.CopyFile(importFilePath, exportFilePath)
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(exportFilePath)
|
||||
|
||||
downloadPath, err := filepath.Abs(exportName)
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(downloadPath)
|
||||
f, err := os.Create(downloadPath)
|
||||
s.Require().Nil(err)
|
||||
defer f.Close()
|
||||
|
||||
err = exportDownloadCmdF(c, cmd, []string{exportName, downloadPath})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().True(strings.HasPrefix(printer.GetLines()[0].(string), "Export file downloaded to "))
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
info, err := os.Stat(downloadPath)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(fs.FileMode(0600), info.Mode().Perm(), fmt.Sprintf("expected %o, got %o", fs.FileMode(0600), info.Mode().Perm()))
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3842 - full download", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("num-retries", 5, "")
|
||||
|
||||
exportFilePath := filepath.Join(exportPath, exportName)
|
||||
err := utils.CopyFile(importFilePath, exportFilePath)
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(exportFilePath)
|
||||
|
||||
downloadPath, err := filepath.Abs(exportName)
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(downloadPath)
|
||||
|
||||
err = exportDownloadCmdF(c, cmd, []string{exportName, downloadPath})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().True(strings.HasPrefix(printer.GetLines()[0].(string), "Export file downloaded to "))
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
expected, err := os.ReadFile(exportFilePath)
|
||||
s.Require().Nil(err)
|
||||
actual, err := os.ReadFile(downloadPath)
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Equal(expected, actual)
|
||||
|
||||
info, err := os.Stat(downloadPath)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(fs.FileMode(0600), info.Mode().Perm(), fmt.Sprintf("expected %o, got %o", fs.FileMode(0600), info.Mode().Perm()))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExportJobShowCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
s.Run("MM-T3885 - no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
job1, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := exportJobShowCmdF(s.th.Client, &cobra.Command{}, []string{job1.Id})
|
||||
s.Require().EqualError(err, "failed to get export job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3886 - not found", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := exportJobShowCmdF(c, &cobra.Command{}, []string{model.NewId()})
|
||||
s.Require().ErrorContains(err, "failed to get export job: Unable to get the job.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3841 - found", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := exportJobShowCmdF(c, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(job, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExportJobListCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("MM-T3887 - no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
err := exportJobListCmdF(s.th.Client, cmd, nil)
|
||||
s.Require().EqualError(err, "failed to get jobs: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3888 - no export jobs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
err := exportJobListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3840 - some export jobs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 2
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
_, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job2, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job3, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := exportJobListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), perPage)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(job3, printer.GetLines()[0].(*model.Job))
|
||||
s.Require().Equal(job2, printer.GetLines()[1].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExportJobCancelCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("Cancel an export job without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
err := exportJobCancelCmdF(s.th.Client, cmd, []string{job.Id})
|
||||
s.Require().EqualError(err, "failed to get export job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("No export jobs to cancel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := exportJobCancelCmdF(c, cmd, []string{model.NewId()})
|
||||
s.Require().ErrorContains(err, "failed to get export job: Unable to get the job.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Cancel an export job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
job1, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job2, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := exportJobCancelCmdF(c, cmd, []string{job1.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
// Get job1 again to refresh its status
|
||||
job1, appErr = s.th.App.GetJob(s.th.Context, job1.Id)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
// Get job2 again to ensure its status did not change
|
||||
job2, _ = s.th.App.GetJob(s.th.Context, job2.Id)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.Require().Equal(job1.Status, model.JobStatusCanceled)
|
||||
s.Require().NotEqual(job2.Status, model.JobStatusCanceled)
|
||||
})
|
||||
}
|
||||
149
server/cmd/mmctl/commands/export_test.go
Normal file
149
server/cmd/mmctl/commands/export_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestExportCreateCmdF() {
|
||||
s.Run("create export", func() {
|
||||
printer.Clean()
|
||||
mockJob := &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
Data: map[string]string{
|
||||
"include_attachments": "true",
|
||||
"include_roles_and_schemes": "true",
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateJob(context.TODO(), mockJob).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := exportCreateCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
|
||||
s.Run("create export without attachments", func() {
|
||||
printer.Clean()
|
||||
mockJob := &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
Data: map[string]string{
|
||||
"include_roles_and_schemes": "true",
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateJob(context.TODO(), mockJob).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("no-attachments", true, "")
|
||||
|
||||
err := exportCreateCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
|
||||
s.Run("create export without roles and schemes", func() {
|
||||
printer.Clean()
|
||||
mockJob := &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
Data: map[string]string{
|
||||
"include_attachments": "true",
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateJob(context.TODO(), mockJob).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("no-roles-and-schemes", true, "")
|
||||
|
||||
err := exportCreateCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
}
|
||||
func (s *MmctlUnitTestSuite) TestExportDeleteCmdF() {
|
||||
printer.Clean()
|
||||
|
||||
exportName := "export.zip"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DeleteExport(context.TODO(), exportName).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := exportDeleteCmdF(s.client, &cobra.Command{}, []string{exportName})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal(fmt.Sprintf(`Export file "%s" has been deleted`, exportName), printer.GetLines()[0])
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestExportListCmdF() {
|
||||
s.Run("no exports", func() {
|
||||
printer.Clean()
|
||||
var mockExports []string
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListExports(context.TODO()).
|
||||
Return(mockExports, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := exportListCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal("No export files found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("some exports", func() {
|
||||
printer.Clean()
|
||||
mockExports := []string{
|
||||
"export1.zip",
|
||||
"export2.zip",
|
||||
"export3.zip",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListExports(context.TODO()).
|
||||
Return(mockExports, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := exportListCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), len(mockExports))
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
for i, line := range printer.GetLines() {
|
||||
s.Equal(mockExports[i], line)
|
||||
}
|
||||
})
|
||||
}
|
||||
121
server/cmd/mmctl/commands/extract.go
Normal file
121
server/cmd/mmctl/commands/extract.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ExtractCmd = &cobra.Command{
|
||||
Use: "extract",
|
||||
Short: "Management of content extraction job.",
|
||||
}
|
||||
|
||||
var ExtractRunCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Example: " extract run",
|
||||
Short: "Start a content extraction job.",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(extractRunCmdF),
|
||||
}
|
||||
|
||||
var ExtractJobCmd = &cobra.Command{
|
||||
Use: "job",
|
||||
Short: "List and show content extraction jobs",
|
||||
}
|
||||
|
||||
var ExtractJobListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Example: " extract job list",
|
||||
Short: "List content extraction jobs",
|
||||
Aliases: []string{"ls"},
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(extractJobListCmdF),
|
||||
}
|
||||
|
||||
var ExtractJobShowCmd = &cobra.Command{
|
||||
Use: "show [extractJobID]",
|
||||
Example: " extract job show f3d68qkkm7n8xgsfxwuo498rah",
|
||||
Short: "Show extract job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(extractJobShowCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ExtractRunCmd.Flags().Int64("from", 0, "The timestamp of the earliest file to extract, expressed in seconds since the unix epoch.")
|
||||
ExtractRunCmd.Flags().Int64("to", 0, "The timestamp of the latest file to extract, expressed in seconds since the unix epoch. Defaults to the current time.")
|
||||
ExtractJobListCmd.Flags().Int("page", 0, "Page number to fetch for the list of extract jobs")
|
||||
ExtractJobListCmd.Flags().Int("per-page", DefaultPageSize, "Number of extract jobs to be fetched")
|
||||
ExtractJobListCmd.Flags().Bool("all", false, "Fetch all extract jobs. --page flag will be ignore if provided")
|
||||
ExtractJobCmd.AddCommand(
|
||||
ExtractJobListCmd,
|
||||
ExtractJobShowCmd,
|
||||
)
|
||||
ExtractCmd.AddCommand(
|
||||
ExtractRunCmd,
|
||||
ExtractJobCmd,
|
||||
)
|
||||
RootCmd.AddCommand(ExtractCmd)
|
||||
}
|
||||
|
||||
func extractRunCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
from, err := command.Flags().GetInt64("from")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to, err := command.Flags().GetInt64("to")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if to == 0 {
|
||||
to = model.GetMillis() / 1000
|
||||
}
|
||||
|
||||
job, _, err := c.CreateJob(context.TODO(), &model.Job{
|
||||
Type: model.JobTypeExtractContent,
|
||||
Data: map[string]string{
|
||||
"from": strconv.FormatInt(from, 10),
|
||||
"to": strconv.FormatInt(to, 10),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create content extraction job: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Content extraction job successfully created, ID: {{.Id}}", job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractJobShowCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
job, _, err := c.GetJob(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get content extraction job: %w", err)
|
||||
}
|
||||
printExtractContentJob(job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractJobListCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
return jobListCmdF(c, command, model.JobTypeExtractContent, "")
|
||||
}
|
||||
|
||||
func printExtractContentJob(job *model.Job) {
|
||||
if job.StartAt > 0 {
|
||||
printer.PrintT(fmt.Sprintf(" ID: {{.Id}}\n Status: {{.Status}}\n Created: %s\n Started: %s\n Processed: %s\n Errors: %s\n",
|
||||
time.Unix(job.CreateAt/1000, 0), time.Unix(job.StartAt/1000, 0), job.Data["processed"], job.Data["errors"]), job)
|
||||
} else {
|
||||
printer.PrintT(fmt.Sprintf(" ID: {{.Id}}\n Status: {{.Status}}\n Created: %s\n\n",
|
||||
time.Unix(job.CreateAt/1000, 0)), job)
|
||||
}
|
||||
}
|
||||
190
server/cmd/mmctl/commands/extract_e2e_test.go
Normal file
190
server/cmd/mmctl/commands/extract_e2e_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExtractRunCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
docName := "sample-doc.pdf"
|
||||
docFilePath := filepath.Join(server.GetPackagePath(), "tests", docName)
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int64("from", 0, "")
|
||||
cmd.Flags().Int64("to", model.GetMillis()/1000, "")
|
||||
|
||||
err := extractRunCmdF(s.th.Client, cmd, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("failed to create content extraction job: You do not have the appropriate permissions.", err.Error())
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("run extraction job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
file, err := os.Open(docFilePath)
|
||||
s.Require().NoError(err)
|
||||
defer file.Close()
|
||||
|
||||
info, err := file.Stat()
|
||||
s.Require().NoError(err)
|
||||
|
||||
us, _, err := s.th.SystemAdminClient.CreateUpload(context.Background(), &model.UploadSession{
|
||||
ChannelId: s.th.BasicChannel.Id,
|
||||
Filename: info.Name(),
|
||||
FileSize: info.Size(),
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(us)
|
||||
|
||||
_, _, err = s.th.SystemAdminClient.UploadData(context.Background(), us.Id, file)
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int64("from", 0, "")
|
||||
cmd.Flags().Int64("to", model.GetMillis()/1000, "")
|
||||
|
||||
err = extractRunCmdF(c, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExtractJobShowCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExtractContent,
|
||||
Data: map[string]string{},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
job1, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExtractContent,
|
||||
Data: map[string]string{},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := extractJobShowCmdF(s.th.Client, &cobra.Command{}, []string{job1.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("failed to get content extraction job: You do not have the appropriate permissions.", err.Error())
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("not found", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := extractJobShowCmdF(c, &cobra.Command{}, []string{model.NewId()})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, "failed to get content extraction job: Unable to get the job.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("found", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := extractJobShowCmdF(c, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(job, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestExtractJobListCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
err := extractJobListCmdF(s.th.Client, cmd, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("failed to get jobs: You do not have the appropriate permissions.", err.Error())
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("no content extraction jobs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
err := extractJobListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("some content extraction jobs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 2
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
_, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExtractContent,
|
||||
Data: map[string]string{},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job2, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExtractContent,
|
||||
Data: map[string]string{},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job3, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExtractContent,
|
||||
Data: map[string]string{},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
err := extractJobListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), perPage)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(job3, printer.GetLines()[0].(*model.Job))
|
||||
s.Require().Equal(job2, printer.GetLines()[1].(*model.Job))
|
||||
})
|
||||
}
|
||||
346
server/cmd/mmctl/commands/group.go
Normal file
346
server/cmd/mmctl/commands/group.go
Normal file
@@ -0,0 +1,346 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var GroupCmd = &cobra.Command{
|
||||
Use: "group",
|
||||
Short: "Management of groups",
|
||||
}
|
||||
|
||||
var ListLdapGroupsCmd = &cobra.Command{
|
||||
Use: "list-ldap",
|
||||
Short: "List LDAP groups",
|
||||
Example: " group list-ldap",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(listLdapGroupsCmdF),
|
||||
}
|
||||
|
||||
var ChannelGroupCmd = &cobra.Command{
|
||||
Use: "channel",
|
||||
Short: "Management of channel groups",
|
||||
}
|
||||
|
||||
var ChannelGroupEnableCmd = &cobra.Command{
|
||||
Use: "enable [team]:[channel]",
|
||||
Short: "Enables group constrains in the specified channel",
|
||||
Example: " group channel enable myteam:mychannel",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(channelGroupEnableCmdF),
|
||||
}
|
||||
|
||||
var ChannelGroupDisableCmd = &cobra.Command{
|
||||
Use: "disable [team]:[channel]",
|
||||
Short: "Disables group constrains in the specified channel",
|
||||
Example: " group channel disable myteam:mychannel",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(channelGroupDisableCmdF),
|
||||
}
|
||||
|
||||
// ChannelGroupStatusCmd is a command which outputs group constrain status for a channel
|
||||
var ChannelGroupStatusCmd = &cobra.Command{
|
||||
Use: "status [team]:[channel]",
|
||||
Short: "Show's the group constrain status for the specified channel",
|
||||
Example: " group channel status myteam:mychannel",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(channelGroupStatusCmdF),
|
||||
}
|
||||
|
||||
var ChannelGroupListCmd = &cobra.Command{
|
||||
Use: "list [team]:[channel]",
|
||||
Short: "List channel groups",
|
||||
Long: "List the groups associated with a channel",
|
||||
Example: " group channel list myteam:mychannel",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(channelGroupListCmdF),
|
||||
}
|
||||
|
||||
var TeamGroupCmd = &cobra.Command{
|
||||
Use: "team",
|
||||
Short: "Management of team groups",
|
||||
}
|
||||
|
||||
var TeamGroupEnableCmd = &cobra.Command{
|
||||
Use: "enable [team]",
|
||||
Short: "Enables group constrains in the specified team",
|
||||
Example: " group team enable myteam",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(teamGroupEnableCmdF),
|
||||
}
|
||||
|
||||
var TeamGroupDisableCmd = &cobra.Command{
|
||||
Use: "disable [team]",
|
||||
Short: "Disables group constrains in the specified team",
|
||||
Example: " group team disable myteam",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(teamGroupDisableCmdF),
|
||||
}
|
||||
|
||||
var TeamGroupStatusCmd = &cobra.Command{
|
||||
Use: "status [team]",
|
||||
Short: "Show's the group constrain status for the specified team",
|
||||
Example: " group team status myteam",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(teamGroupStatusCmdF),
|
||||
}
|
||||
|
||||
var TeamGroupListCmd = &cobra.Command{
|
||||
Use: "list [team]",
|
||||
Short: "List team groups",
|
||||
Long: "List the groups associated with a team",
|
||||
Example: " group team list myteam",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(teamGroupListCmdF),
|
||||
}
|
||||
|
||||
var UserGroupCmd = &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "Management of custom user groups",
|
||||
}
|
||||
|
||||
var UserGroupRestoreCmd = &cobra.Command{
|
||||
Use: "restore [groupname]",
|
||||
Short: "Restore user group",
|
||||
Long: "Restore deleted custom user group",
|
||||
Example: " group user restore examplegroup",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(userGroupRestoreCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ChannelGroupCmd.AddCommand(
|
||||
ChannelGroupEnableCmd,
|
||||
ChannelGroupDisableCmd,
|
||||
ChannelGroupStatusCmd,
|
||||
ChannelGroupListCmd,
|
||||
)
|
||||
|
||||
TeamGroupCmd.AddCommand(
|
||||
TeamGroupEnableCmd,
|
||||
TeamGroupDisableCmd,
|
||||
TeamGroupStatusCmd,
|
||||
TeamGroupListCmd,
|
||||
)
|
||||
|
||||
UserGroupCmd.AddCommand(
|
||||
UserGroupRestoreCmd,
|
||||
)
|
||||
|
||||
GroupCmd.AddCommand(
|
||||
ListLdapGroupsCmd,
|
||||
ChannelGroupCmd,
|
||||
TeamGroupCmd,
|
||||
UserGroupCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(GroupCmd)
|
||||
}
|
||||
|
||||
func listLdapGroupsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
groups, _, err := c.GetLdapGroups(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
printer.PrintT("{{.DisplayName}}", group)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func channelGroupEnableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.New("Unable to find channel '" + args[0] + "'")
|
||||
}
|
||||
|
||||
groupOpts := &model.GroupSearchOpts{
|
||||
PageOpts: &model.PageOpts{
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
},
|
||||
}
|
||||
|
||||
groups, _, _, err := c.GetGroupsByChannel(context.TODO(), channel.Id, *groupOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(groups) == 0 {
|
||||
return errors.New("Channel '" + args[0] + "' has no groups associated. It cannot be group-constrained")
|
||||
}
|
||||
|
||||
channelPatch := model.ChannelPatch{GroupConstrained: new(true)}
|
||||
if _, _, err = c.PatchChannel(context.TODO(), channel.Id, &channelPatch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func channelGroupDisableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.New("Unable to find channel '" + args[0] + "'")
|
||||
}
|
||||
|
||||
channelPatch := model.ChannelPatch{GroupConstrained: new(false)}
|
||||
if _, _, err := c.PatchChannel(context.TODO(), channel.Id, &channelPatch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func channelGroupStatusCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.New("Unable to find channel '" + args[0] + "'")
|
||||
}
|
||||
|
||||
if channel.GroupConstrained != nil && *channel.GroupConstrained {
|
||||
printer.Print("Enabled")
|
||||
} else {
|
||||
printer.Print("Disabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func channelGroupListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.New("Unable to find channel '" + args[0] + "'")
|
||||
}
|
||||
|
||||
groupOpts := model.GroupSearchOpts{
|
||||
PageOpts: &model.PageOpts{
|
||||
Page: 0,
|
||||
PerPage: 9999,
|
||||
},
|
||||
}
|
||||
groups, _, _, err := c.GetGroupsByChannel(context.TODO(), channel.Id, groupOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
printer.PrintT("{{.DisplayName}}", group)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func teamGroupEnableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
team := getTeamFromTeamArg(c, args[0])
|
||||
if team == nil {
|
||||
return errors.New("Unable to find team '" + args[0] + "'")
|
||||
}
|
||||
|
||||
groupOpts := model.GroupSearchOpts{
|
||||
PageOpts: &model.PageOpts{
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
},
|
||||
}
|
||||
groups, _, _, err := c.GetGroupsByTeam(context.TODO(), team.Id, groupOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(groups) == 0 {
|
||||
return errors.New("Team '" + args[0] + "' has no groups associated. It cannot be group-constrained")
|
||||
}
|
||||
|
||||
teamPatch := model.TeamPatch{GroupConstrained: new(true)}
|
||||
if _, _, err = c.PatchTeam(context.TODO(), team.Id, &teamPatch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func teamGroupDisableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
team := getTeamFromTeamArg(c, args[0])
|
||||
if team == nil {
|
||||
return errors.New("Unable to find team '" + args[0] + "'")
|
||||
}
|
||||
|
||||
teamPatch := model.TeamPatch{GroupConstrained: new(false)}
|
||||
if _, _, err := c.PatchTeam(context.TODO(), team.Id, &teamPatch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func teamGroupStatusCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
team := getTeamFromTeamArg(c, args[0])
|
||||
if team == nil {
|
||||
return errors.New("Unable to find team '" + args[0] + "'")
|
||||
}
|
||||
|
||||
if team.GroupConstrained != nil && *team.GroupConstrained {
|
||||
printer.Print("Enabled")
|
||||
} else {
|
||||
printer.Print("Disabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func teamGroupListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
team := getTeamFromTeamArg(c, args[0])
|
||||
if team == nil {
|
||||
return errors.New("Unable to find team '" + args[0] + "'")
|
||||
}
|
||||
|
||||
groupOpts := model.GroupSearchOpts{
|
||||
PageOpts: &model.PageOpts{
|
||||
Page: 0,
|
||||
PerPage: 9999,
|
||||
},
|
||||
}
|
||||
groups, _, _, err := c.GetGroupsByTeam(context.TODO(), team.Id, groupOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
printer.PrintT("{{.DisplayName}}", group)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func userGroupRestoreCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
groupID := args[0]
|
||||
_, resp, err := c.RestoreGroup(context.TODO(), groupID, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
printer.Print("Group successfully restored with ID: " + groupID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
557
server/cmd/mmctl/commands/group_e2e_test.go
Normal file
557
server/cmd/mmctl/commands/group_e2e_test.go
Normal file
@@ -0,0 +1,557 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestChannelGroupEnableCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
channelName := api4.GenerateTestChannelName()
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: channelName,
|
||||
DisplayName: "dn_" + channelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.DeleteChannel(s.th.Context, channel, "")
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
id := model.NewId()
|
||||
group, appErr := s.th.App.CreateGroup(&model.Group{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: new("name" + id),
|
||||
Source: model.GroupSourceLdap,
|
||||
Description: "description_" + id,
|
||||
RemoteId: new(model.NewId()),
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
_, err := s.th.App.DeleteGroup(group.Id)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
_, appErr = s.th.App.UpsertGroupSyncable(&model.GroupSyncable{
|
||||
GroupId: group.Id,
|
||||
SyncableId: channel.Id,
|
||||
Type: model.GroupSyncableTypeChannel,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
_, err := s.th.App.DeleteGroupSyncable(group.Id, channel.Id, model.GroupSyncableTypeChannel)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Run("Should not allow regular user to enable group for channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := channelGroupEnableCmdF(s.th.Client, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + channelName})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Should enable group sync for the channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := channelGroupEnableCmdF(c, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + channelName})
|
||||
s.Require().NoError(err)
|
||||
|
||||
channel.GroupConstrained = new(false)
|
||||
defer func() {
|
||||
_, err := s.th.App.UpdateChannel(s.th.Context, channel)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
ch, appErr := s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().True(ch.IsGroupConstrained())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestChannelGroupDisableCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
channelName := api4.GenerateTestChannelName()
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: channelName,
|
||||
DisplayName: "dn_" + channelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.DeleteChannel(s.th.Context, channel, "")
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
id := model.NewId()
|
||||
group, appErr := s.th.App.CreateGroup(&model.Group{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: new("name" + id),
|
||||
Source: model.GroupSourceLdap,
|
||||
Description: "description_" + id,
|
||||
RemoteId: new(model.NewId()),
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
_, err := s.th.App.DeleteGroup(group.Id)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
_, appErr = s.th.App.UpsertGroupSyncable(&model.GroupSyncable{
|
||||
GroupId: group.Id,
|
||||
SyncableId: channel.Id,
|
||||
Type: model.GroupSyncableTypeChannel,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
_, err := s.th.App.DeleteGroupSyncable(group.Id, channel.Id, model.GroupSyncableTypeChannel)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
channel.GroupConstrained = new(true)
|
||||
defer func() {
|
||||
_, err := s.th.App.UpdateChannel(s.th.Context, channel)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Run("Should not allow regular user to disable group for channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := channelGroupDisableCmdF(s.th.Client, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + channelName})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Should disable group sync for the channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := channelGroupDisableCmdF(c, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + channelName})
|
||||
s.Require().NoError(err)
|
||||
|
||||
channel.GroupConstrained = new(true)
|
||||
defer func() {
|
||||
_, err := s.th.App.UpdateChannel(s.th.Context, channel)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
ch, appErr := s.th.App.GetChannel(s.th.Context, channel.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().False(ch.IsGroupConstrained())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestListLdapGroupsCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
configForLdap(s.th)
|
||||
|
||||
s.Run("MM-T3977 Should not allow regular user to list LDAP groups", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := listLdapGroupsCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3976 Should list LDAP groups", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
// we rely on the test data generated for the openldap server
|
||||
// i.e. "test-data.ldif" script
|
||||
err := listLdapGroupsCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotEmpty(printer.GetLines())
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestChannelGroupStatusCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
channelName := api4.GenerateTestChannelName()
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: channelName,
|
||||
DisplayName: "dn_" + channelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
GroupConstrained: new(true),
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.DeleteChannel(s.th.Context, channel, "")
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
channelName2 := api4.GenerateTestChannelName()
|
||||
channel2, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: channelName2,
|
||||
DisplayName: "dn_" + channelName2,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.DeleteChannel(s.th.Context, channel2, "")
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.RunForAllClients("MM-T3974 Should allow to get status of a group constrained channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := channelGroupStatusCmdF(c, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + channelName})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Enabled")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForAllClients("MM-T3975 Should allow to get status of a regular channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := channelGroupStatusCmdF(c, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + channelName2})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Disabled")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestChannelGroupListCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
channelName := api4.GenerateTestChannelName()
|
||||
channel, appErr := s.th.App.CreateChannel(s.th.Context, &model.Channel{
|
||||
TeamId: s.th.BasicTeam.Id,
|
||||
Name: channelName,
|
||||
DisplayName: "dn_" + channelName,
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, false)
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.DeleteChannel(s.th.Context, channel, "")
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
id := model.NewId()
|
||||
group, appErr := s.th.App.CreateGroup(&model.Group{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: new("name" + id),
|
||||
Source: model.GroupSourceLdap,
|
||||
Description: "description_" + id,
|
||||
RemoteId: new(model.NewId()),
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
_, err := s.th.App.DeleteGroup(group.Id)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
_, appErr = s.th.App.UpsertGroupSyncable(&model.GroupSyncable{
|
||||
GroupId: group.Id,
|
||||
SyncableId: channel.Id,
|
||||
Type: model.GroupSyncableTypeChannel,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
_, err := s.th.App.DeleteGroupSyncable(group.Id, channel.Id, model.GroupSyncableTypeChannel)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Run("MM-T3970 Should not allow regular user to get list of LDAP groups in a channel", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := channelGroupListCmdF(s.th.Client, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + channelName})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3969 Should allow to get list of LDAP groups in a channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := channelGroupListCmdF(c, &cobra.Command{}, []string{s.th.BasicTeam.Name + ":" + channelName})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
gs, ok := printer.GetLines()[0].(*model.GroupWithSchemeAdmin)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(gs.Group, *group)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestTeamGroupDisableCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
team, _, cleanUpFn := createTestGroupTeam(s)
|
||||
defer cleanUpFn()
|
||||
|
||||
team.GroupConstrained = new(true)
|
||||
_, err := s.th.App.UpdateTeam(team)
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Run("MM-T3919 Should not allow regular user to disable group for team", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupDisableCmdF(s.th.Client, &cobra.Command{}, []string{team.Name})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3920 Should disable group sync for the team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupDisableCmdF(c, &cobra.Command{}, []string{team.Name})
|
||||
s.Require().NoError(err)
|
||||
|
||||
team.GroupConstrained = new(true)
|
||||
defer func() {
|
||||
_, err := s.th.App.UpdateTeam(team)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
tm, appErr := s.th.App.GetTeam(team.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().False(tm.IsGroupConstrained())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestTeamGroupEnableCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
team, _, cleanUpFn := createTestGroupTeam(s)
|
||||
defer cleanUpFn()
|
||||
|
||||
s.Run("MM-T3917 Should not allow regular user to enable group for team", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupEnableCmdF(s.th.Client, &cobra.Command{}, []string{team.Name})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3918 Should enable group sync for the team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupEnableCmdF(c, &cobra.Command{}, []string{team.Name})
|
||||
s.Require().NoError(err)
|
||||
|
||||
team.GroupConstrained = new(false)
|
||||
defer func() {
|
||||
_, err := s.th.App.UpdateTeam(team)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
tm, appErr := s.th.App.GetTeam(team.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().True(tm.IsGroupConstrained())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestTeamGroupStatusCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
team, _, cleanUpFn := createTestGroupTeam(s)
|
||||
defer func() {
|
||||
cleanUpFn()
|
||||
}()
|
||||
|
||||
teamName2 := api4.GenerateTestTeamName()
|
||||
team2, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
Name: teamName2,
|
||||
DisplayName: "dn_" + teamName2,
|
||||
Type: model.TeamInvite,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
err := s.th.App.PermanentDeleteTeam(s.th.Context, team2)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Run("MM-T3921 Should not allow regular user to get status of LDAP groups in a team where they are not a member of", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupStatusCmdF(s.th.Client, &cobra.Command{}, []string{team.Name})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, team.Id, s.th.BasicUser.Id, s.th.SystemAdminUser.Id)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, _, appErr = s.th.App.AddUserToTeam(s.th.Context, team2.Id, s.th.BasicUser.Id, s.th.SystemAdminUser.Id)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.RunForAllClients("MM-T3922 Should allow to get status of a group constrained team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupStatusCmdF(c, &cobra.Command{}, []string{team.Name})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Enabled")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForAllClients("MM-T3923 Should allow to get status of a regular team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupStatusCmdF(c, &cobra.Command{}, []string{teamName2})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Disabled")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestTeamGroupListCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
team, group, cleanUpFn := createTestGroupTeam(s)
|
||||
defer func() {
|
||||
cleanUpFn()
|
||||
}()
|
||||
|
||||
s.Run("MM-T3924 Should not allow regular user to get list of LDAP groups in a team", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupListCmdF(s.th.Client, &cobra.Command{}, []string{team.Name})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3925 Should allow to get list of LDAP groups in a team", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := teamGroupListCmdF(c, &cobra.Command{}, []string{team.Name})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
gs, ok := printer.GetLines()[0].(*model.GroupWithSchemeAdmin)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(gs.Group, *group)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func createTestGroupTeam(s *MmctlE2ETestSuite) (*model.Team, *model.Group, func()) {
|
||||
teamName := api4.GenerateTestTeamName()
|
||||
team, appErr := s.th.App.CreateTeam(s.th.Context, &model.Team{
|
||||
Name: teamName,
|
||||
DisplayName: "dn_" + teamName,
|
||||
Type: model.TeamOpen,
|
||||
GroupConstrained: new(true),
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
id := model.NewId()
|
||||
group, appErr := s.th.App.CreateGroup(&model.Group{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: new("name" + id),
|
||||
Source: model.GroupSourceLdap,
|
||||
Description: "description_" + id,
|
||||
RemoteId: new(model.NewId()),
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
_, appErr = s.th.App.UpsertGroupSyncable(&model.GroupSyncable{
|
||||
GroupId: group.Id,
|
||||
SyncableId: team.Id,
|
||||
Type: model.GroupSyncableTypeTeam,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
cleanUpFn := func() {
|
||||
_, err := s.th.App.DeleteGroupSyncable(group.Id, team.Id, model.GroupSyncableTypeTeam)
|
||||
s.Require().Nil(err)
|
||||
|
||||
_, err = s.th.App.DeleteGroup(group.Id)
|
||||
s.Require().Nil(err)
|
||||
|
||||
err = s.th.App.PermanentDeleteTeamId(s.th.Context, team.Id)
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
|
||||
return team, group, cleanUpFn
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestUserGroupRestoreCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
// create group
|
||||
id := model.NewId()
|
||||
group, appErr := s.th.App.CreateGroup(&model.Group{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: new("name" + id),
|
||||
Source: model.GroupSourceCustom,
|
||||
Description: "description_" + id,
|
||||
RemoteId: new(model.NewId()),
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
s.th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional, "ldap"))
|
||||
|
||||
defer func() {
|
||||
_, err := s.th.App.DeleteGroup(group.Id)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
s.Run("Should allow group restore after deletion", func() {
|
||||
printer.Clean()
|
||||
|
||||
_, appErr := s.th.App.DeleteGroup(group.Id)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.th.RemovePermissionFromRole(s.T(), model.PermissionRestoreCustomGroup.Id, model.SystemUserRoleId)
|
||||
err := userGroupRestoreCmdF(s.th.Client, &cobra.Command{}, []string{group.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(err.Error(), "You do not have the appropriate permissions.")
|
||||
|
||||
s.th.AddPermissionToRole(s.T(), model.PermissionRestoreCustomGroup.Id, model.SystemUserRoleId)
|
||||
err = userGroupRestoreCmdF(s.th.Client, &cobra.Command{}, []string{group.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0].(string), "Group successfully restored with ID: "+group.Id)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// shouldn't allow restoring of active groups
|
||||
printer.Clean()
|
||||
err = userGroupRestoreCmdF(s.th.Client, &cobra.Command{}, []string{group.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(err.Error(), "no matching group found")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
1579
server/cmd/mmctl/commands/group_test.go
Normal file
1579
server/cmd/mmctl/commands/group_test.go
Normal file
File diff suppressed because it is too large
Load Diff
578
server/cmd/mmctl/commands/import.go
Normal file
578
server/cmd/mmctl/commands/import.go
Normal file
@@ -0,0 +1,578 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/commands/importer"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
var ImportCmd = &cobra.Command{
|
||||
Use: "import",
|
||||
Short: "Management of imports",
|
||||
}
|
||||
|
||||
var ImportUploadCmd = &cobra.Command{
|
||||
Use: "upload [filepath]",
|
||||
Short: "Upload import files",
|
||||
Example: " import upload import_file.zip",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(importUploadCmdF),
|
||||
}
|
||||
|
||||
var ImportDeleteCmd = &cobra.Command{
|
||||
Use: "delete [importname]",
|
||||
Short: "Delete an import file",
|
||||
Example: " import delete import_file.zip",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(importDeleteCmdF),
|
||||
}
|
||||
|
||||
var ImportListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List import files",
|
||||
Example: " import list",
|
||||
}
|
||||
|
||||
var ImportListAvailableCmd = &cobra.Command{
|
||||
Use: "available",
|
||||
Short: "List available import files",
|
||||
Example: " import list available",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(importListAvailableCmdF),
|
||||
}
|
||||
|
||||
var ImportJobCmd = &cobra.Command{
|
||||
Use: "job",
|
||||
Short: "List and show import jobs",
|
||||
}
|
||||
|
||||
var ImportListIncompleteCmd = &cobra.Command{
|
||||
Use: "incomplete",
|
||||
Short: "List incomplete import files uploads",
|
||||
Example: " import list incomplete",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(importListIncompleteCmdF),
|
||||
}
|
||||
|
||||
var ImportJobListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Example: " import job list",
|
||||
Short: "List import jobs",
|
||||
Aliases: []string{"ls"},
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(importJobListCmdF),
|
||||
}
|
||||
|
||||
var ImportJobShowCmd = &cobra.Command{
|
||||
Use: "show [importJobID]",
|
||||
Example: " import job show f3d68qkkm7n8xgsfxwuo498rah",
|
||||
Short: "Show import job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(importJobShowCmdF),
|
||||
}
|
||||
|
||||
var ImportProcessCmd = &cobra.Command{
|
||||
Use: "process [importname]",
|
||||
Example: " import process 35uy6cwrqfnhdx3genrhqqznxc_import.zip",
|
||||
Short: "Start an import job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(importProcessCmdF),
|
||||
}
|
||||
|
||||
var ImportValidateCmd = &cobra.Command{
|
||||
Use: "validate [filepath]",
|
||||
Example: " import validate import_file.zip --team myteam --team myotherteam",
|
||||
Short: "Validate an import file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
return importValidateCmdF(nil, command, args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ImportUploadCmd.Flags().Bool("resume", false, "Set to true to resume an incomplete import upload.")
|
||||
ImportUploadCmd.Flags().String("upload", "", "The ID of the import upload to resume.")
|
||||
|
||||
ImportJobListCmd.Flags().Int("page", 0, "Page number to fetch for the list of import jobs")
|
||||
ImportJobListCmd.Flags().Int("per-page", DefaultPageSize, "Number of import jobs to be fetched")
|
||||
ImportJobListCmd.Flags().Bool("all", false, "Fetch all import jobs. --page flag will be ignore if provided")
|
||||
|
||||
ImportValidateCmd.Flags().StringArray("team", nil, "Predefined team[s] to assume as already present on the destination server. Implies --check-missing-teams. The flag can be repeated")
|
||||
ImportValidateCmd.Flags().Bool("check-missing-teams", false, "Check for teams that are not defined but referenced in the archive")
|
||||
ImportValidateCmd.Flags().Bool("ignore-attachments", false, "Don't check if the attached files are present in the archive")
|
||||
ImportValidateCmd.Flags().Bool("check-server-duplicates", true, "Set to false to ignore teams, channels, and users already present on the server")
|
||||
|
||||
ImportProcessCmd.Flags().Bool("bypass-upload", false, "If this is set, the file is not processed from the server, but rather directly read from the filesystem. Works only in --local mode.")
|
||||
ImportProcessCmd.Flags().Bool("extract-content", true, "If this is set, document attachments will be extracted and indexed during the import process. It is advised to disable it to improve performance.")
|
||||
ImportProcessCmd.Flags().Int("workers", 0, "The number of concurrent import worker goroutines. Controls database load during import. When set to 0 (default), uses the number of CPUs available. Maximum allowed is 4x the CPU count.")
|
||||
|
||||
ImportListCmd.AddCommand(
|
||||
ImportListAvailableCmd,
|
||||
ImportListIncompleteCmd,
|
||||
)
|
||||
ImportJobCmd.AddCommand(
|
||||
ImportJobListCmd,
|
||||
ImportJobShowCmd,
|
||||
)
|
||||
ImportCmd.AddCommand(
|
||||
ImportUploadCmd,
|
||||
ImportListCmd,
|
||||
ImportProcessCmd,
|
||||
ImportJobCmd,
|
||||
ImportValidateCmd,
|
||||
ImportDeleteCmd,
|
||||
)
|
||||
RootCmd.AddCommand(ImportCmd)
|
||||
}
|
||||
|
||||
func importListIncompleteCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
isLocal, _ := command.Flags().GetBool("local")
|
||||
userID := "me"
|
||||
if isLocal {
|
||||
userID = model.UploadNoUserID
|
||||
}
|
||||
|
||||
uploads, _, err := c.GetUploadsForUser(context.TODO(), userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get uploads: %w", err)
|
||||
}
|
||||
|
||||
var hasImports bool
|
||||
for _, us := range uploads {
|
||||
if us.Type == model.UploadTypeImport {
|
||||
completedPct := float64(us.FileOffset) / float64(us.FileSize) * 100
|
||||
printer.PrintT(fmt.Sprintf(" ID: {{.Id}}\n Name: {{.Filename}}\n Uploaded: {{.FileOffset}}/{{.FileSize}} (%0.0f%%)\n", completedPct), us)
|
||||
hasImports = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasImports {
|
||||
printer.Print("No incomplete import uploads found")
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importListAvailableCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
imports, _, err := c.ListImports(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list imports: %w", err)
|
||||
}
|
||||
|
||||
if len(imports) == 0 {
|
||||
printer.Print("No import files found")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, name := range imports {
|
||||
printer.Print(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importUploadCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
filepath := args[0]
|
||||
|
||||
isLocal, _ := command.Flags().GetBool("local")
|
||||
if isLocal {
|
||||
printer.PrintWarning("In --local mode, you don't need to upload the file to server any more. Directly use the import process command with the --bypass-upload flag and pass the export file.")
|
||||
}
|
||||
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open import file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat import file: %w", err)
|
||||
}
|
||||
|
||||
shouldResume, _ := command.Flags().GetBool("resume")
|
||||
var us *model.UploadSession
|
||||
if shouldResume {
|
||||
uploadID, nErr := command.Flags().GetString("upload")
|
||||
if nErr != nil || !model.IsValidId(uploadID) {
|
||||
return errors.New("upload session ID is missing or invalid")
|
||||
}
|
||||
|
||||
us, _, err = c.GetUpload(context.TODO(), uploadID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get upload session: %w", err)
|
||||
}
|
||||
|
||||
if us.FileSize != info.Size() {
|
||||
return fmt.Errorf("file sizes do not match")
|
||||
}
|
||||
|
||||
if _, nErr := file.Seek(us.FileOffset, io.SeekStart); nErr != nil {
|
||||
return fmt.Errorf("failed to get seek file: %w", nErr)
|
||||
}
|
||||
} else {
|
||||
isLocal, _ := command.Flags().GetBool("local")
|
||||
userID := "me"
|
||||
if isLocal {
|
||||
userID = model.UploadNoUserID
|
||||
}
|
||||
|
||||
us, _, err = c.CreateUpload(context.TODO(), &model.UploadSession{
|
||||
Filename: info.Name(),
|
||||
FileSize: info.Size(),
|
||||
Type: model.UploadTypeImport,
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create upload session: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Upload session successfully created, ID: {{.Id}} ", us)
|
||||
}
|
||||
|
||||
finfo, _, err := c.UploadData(context.TODO(), us.Id, file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload data: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Import file successfully uploaded, name: {{.Id}}", finfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importDeleteCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
importName := args[0]
|
||||
|
||||
if _, err := c.DeleteImport(context.TODO(), importName); err != nil {
|
||||
return fmt.Errorf("failed to delete import: %w", err)
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("Import file %q has been deleted", importName))
|
||||
return nil
|
||||
}
|
||||
|
||||
func importProcessCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
importFile := args[0]
|
||||
|
||||
isLocal, _ := command.Flags().GetBool("local")
|
||||
bypassUpload, _ := command.Flags().GetBool("bypass-upload")
|
||||
if bypassUpload {
|
||||
if isLocal {
|
||||
// First, we validate whether the server is in HA.
|
||||
config, _, err := c.GetClientConfig(context.TODO(), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enableCluster, err := strconv.ParseBool(config["EnableCluster"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse EnableCluster: %w", err)
|
||||
}
|
||||
|
||||
if enableCluster {
|
||||
return errors.New("--bypass-upload flag doesn't work if the server is in HA. Because the file has to be present locally on the server where the job request hits. Please disable HA and try again.")
|
||||
}
|
||||
|
||||
// in local mode, we tell the server to directly read from this file.
|
||||
if _, err := os.Stat(importFile); errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("file %s doesn't exist. NOTE: If this file was uploaded to the server via mmctl import upload, please omit the --bypass-upload flag to revert to old behavior.", importFile)
|
||||
}
|
||||
// If it's not an absolute path, then we make it
|
||||
if !path.IsAbs(importFile) {
|
||||
var err2 error
|
||||
importFile, err2 = filepath.Abs(importFile)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("error is getting the absolute path to %s: %w", importFile, err2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printer.PrintWarning("--bypass-upload has no effect in non-local mode.")
|
||||
}
|
||||
}
|
||||
|
||||
extractContent, _ := command.Flags().GetBool("extract-content")
|
||||
workers, _ := command.Flags().GetInt("workers")
|
||||
|
||||
maxWorkers := runtime.NumCPU() * 4
|
||||
if workers > maxWorkers {
|
||||
return fmt.Errorf("workers value %d exceeds maximum allowed (%d = 4 * CPU count)", workers, maxWorkers)
|
||||
}
|
||||
|
||||
jobData := map[string]string{
|
||||
"import_file": importFile,
|
||||
"local_mode": strconv.FormatBool(isLocal && bypassUpload),
|
||||
"extract_content": strconv.FormatBool(extractContent),
|
||||
}
|
||||
if workers > 0 {
|
||||
jobData["workers"] = strconv.Itoa(workers)
|
||||
}
|
||||
|
||||
job, _, err := c.CreateJob(context.TODO(), &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: jobData,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create import process job: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Import process job successfully created, ID: {{.Id}}", job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importJobShowCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
job, _, err := c.GetJob(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get import job: %w", err)
|
||||
}
|
||||
|
||||
printJob(job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importJobListCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
return jobListCmdF(c, command, model.JobTypeImportProcess, "")
|
||||
}
|
||||
|
||||
type Statistics struct {
|
||||
Roles uint64 `json:"roles"`
|
||||
Schemes uint64 `json:"schemes"`
|
||||
Teams uint64 `json:"teams"`
|
||||
Channels uint64 `json:"channels"`
|
||||
Users uint64 `json:"users"`
|
||||
Emojis uint64 `json:"emojis"`
|
||||
Posts uint64 `json:"posts"`
|
||||
DirectChannels uint64 `json:"direct_channels"`
|
||||
DirectPosts uint64 `json:"direct_posts"`
|
||||
Attachments uint64 `json:"attachments"`
|
||||
}
|
||||
|
||||
type ImportValidationResult struct {
|
||||
FileName string `json:"file_name"`
|
||||
TotalLines uint64 `json:"total_lines"`
|
||||
Elapsed time.Duration `json:"elapsed_time_ns"`
|
||||
Errors []*importer.ImportValidationError `json:"errors"`
|
||||
}
|
||||
|
||||
func importValidateCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
configurePrinter()
|
||||
defer printer.Print("Validation complete\n")
|
||||
|
||||
var (
|
||||
serverTeams = make(map[string]*model.Team) // initialize it in case we need to add teams manually
|
||||
serverChannels map[importer.ChannelTeam]*model.Channel
|
||||
serverUsers map[string]*model.User
|
||||
serverEmails map[string]*model.User
|
||||
maxPostSize int
|
||||
)
|
||||
|
||||
preRunWithClient := func(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
users, err := getPages(func(page, numPerPage int, etag string) ([]*model.User, *model.Response, error) {
|
||||
return c.GetUsers(context.TODO(), page, numPerPage, etag)
|
||||
}, DefaultPageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, _, err := c.GetClientConfig(context.TODO(), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maxPostSize, err = strconv.Atoi(config["MaxPostSize"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse MaxPostSize: %w", err)
|
||||
}
|
||||
|
||||
serverUsers = make(map[string]*model.User)
|
||||
serverEmails = make(map[string]*model.User)
|
||||
for _, user := range users {
|
||||
serverUsers[user.Nickname] = user
|
||||
serverEmails[user.Email] = user
|
||||
}
|
||||
|
||||
teams, err := getPages(func(page, numPerPage int, etag string) ([]*model.Team, *model.Response, error) {
|
||||
return c.GetAllTeams(context.TODO(), etag, page, numPerPage)
|
||||
}, DefaultPageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverChannels = make(map[importer.ChannelTeam]*model.Channel)
|
||||
for _, team := range teams {
|
||||
serverTeams[team.Name] = team
|
||||
|
||||
publicChannels, err := getPages(func(page, numPerPage int, etag string) ([]*model.Channel, *model.Response, error) {
|
||||
return c.GetPublicChannelsForTeam(context.TODO(), team.Id, page, numPerPage, etag)
|
||||
}, DefaultPageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateChannels, err := getPages(func(page, numPerPage int, etag string) ([]*model.Channel, *model.Response, error) {
|
||||
return c.GetPrivateChannelsForTeam(context.TODO(), team.Id, page, numPerPage, etag)
|
||||
}, DefaultPageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, channel := range publicChannels {
|
||||
serverChannels[importer.ChannelTeam{Channel: channel.Name, Team: team.Name}] = channel
|
||||
}
|
||||
for _, channel := range privateChannels {
|
||||
serverChannels[importer.ChannelTeam{Channel: channel.Name, Team: team.Name}] = channel
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if c != nil {
|
||||
err = preRunWithClient(c, command, args)
|
||||
} else {
|
||||
err = withClient(preRunWithClient)(command, args)
|
||||
}
|
||||
if err != nil {
|
||||
printer.Print(fmt.Sprintf("could not initialize client (%s), skipping online checks\n", err.Error()))
|
||||
}
|
||||
|
||||
injectedTeams, err := command.Flags().GetStringArray("team")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, team := range injectedTeams {
|
||||
if _, ok := serverTeams[team]; !ok {
|
||||
serverTeams[team] = &model.Team{
|
||||
Id: "<predefined>",
|
||||
Name: team,
|
||||
DisplayName: "team was predefined",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkMissingTeams, err := command.Flags().GetBool("check-missing-teams")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ignoreAttachments, err := command.Flags().GetBool("ignore-attachments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkServerDuplicates, err := command.Flags().GetBool("check-server-duplicates")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if maxPostSize == 0 {
|
||||
maxPostSize = model.PostMessageMaxRunesV2
|
||||
}
|
||||
|
||||
createMissingTeams := !checkMissingTeams && len(injectedTeams) == 0
|
||||
validator := importer.NewValidator(
|
||||
args[0], // input file
|
||||
ignoreAttachments, // ignore attachments flag
|
||||
createMissingTeams, // create missing teams flag
|
||||
checkServerDuplicates, // check for server duplicates flag
|
||||
serverTeams, // map of existing teams
|
||||
serverChannels, // map of existing channels
|
||||
serverUsers, // map of users by name
|
||||
serverEmails, // map of users by email
|
||||
maxPostSize,
|
||||
)
|
||||
|
||||
var errors []*importer.ImportValidationError
|
||||
templateError := template.Must(template.New("").Parse("{{ .Error }}\n"))
|
||||
validator.OnError(func(ive *importer.ImportValidationError) error {
|
||||
printer.PrintPreparedT(templateError, ive)
|
||||
errors = append(errors, ive)
|
||||
return nil
|
||||
})
|
||||
|
||||
err = validator.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stat := Statistics{
|
||||
Roles: validator.Roles(),
|
||||
Schemes: validator.Schemes(),
|
||||
Teams: validator.TeamCount(),
|
||||
Channels: validator.ChannelCount(),
|
||||
Users: validator.UserCount(),
|
||||
Posts: (validator.PostCount()),
|
||||
DirectChannels: (validator.DirectChannelCount()),
|
||||
DirectPosts: (validator.DirectPostCount()),
|
||||
Emojis: (validator.Emojis()),
|
||||
Attachments: uint64(len(validator.Attachments())),
|
||||
}
|
||||
|
||||
printStatistics(stat)
|
||||
|
||||
createdTeams := validator.CreatedTeams()
|
||||
if createMissingTeams && len(createdTeams) != 0 {
|
||||
printer.PrintT("Automatically created teams: {{ join .CreatedTeams \", \" }}\n", struct {
|
||||
CreatedTeams []string `json:"created_teams"`
|
||||
}{createdTeams})
|
||||
}
|
||||
|
||||
unusedAttachments := validator.UnusedAttachments()
|
||||
if len(unusedAttachments) > 0 {
|
||||
printer.PrintT("Unused Attachments ({{ len .UnusedAttachments }}):\n"+
|
||||
"{{ range .UnusedAttachments }} {{ . }}\n{{ end }}", struct {
|
||||
UnusedAttachments []string `json:"unused_attachments"`
|
||||
}{unusedAttachments})
|
||||
}
|
||||
|
||||
printer.PrintT("It took {{ .Elapsed }} to validate {{ .TotalLines }} lines in {{ .FileName }}\n", ImportValidationResult{args[0], validator.Lines(), validator.Duration(), errors})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configurePrinter() {
|
||||
// we want to manage the newlines ourselves
|
||||
printer.SetNoNewline(true)
|
||||
|
||||
// define a join function
|
||||
printer.SetTemplateFunc("join", strings.Join)
|
||||
}
|
||||
|
||||
func printStatistics(stat Statistics) {
|
||||
tmpl := "\n" +
|
||||
"Roles {{ .Roles }}\n" +
|
||||
"Schemes {{ .Schemes }}\n" +
|
||||
"Teams {{ .Teams }}\n" +
|
||||
"Channels {{ .Channels }}\n" +
|
||||
"Users {{ .Users }}\n" +
|
||||
"Emojis {{ .Emojis }}\n" +
|
||||
"Posts {{ .Posts }}\n" +
|
||||
"Direct Channels {{ .DirectChannels }}\n" +
|
||||
"Direct Posts {{ .DirectPosts }}\n" +
|
||||
"Attachments {{ .Attachments }}\n"
|
||||
|
||||
printer.PrintT(tmpl, stat)
|
||||
}
|
||||
484
server/cmd/mmctl/commands/import_e2e_test.go
Normal file
484
server/cmd/mmctl/commands/import_e2e_test.go
Normal file
@@ -0,0 +1,484 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestImportUploadCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
importName := "import_test.zip"
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "tests", importName)
|
||||
info, err := os.Stat(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := importUploadCmdF(s.th.Client, &cobra.Command{}, []string{importFilePath})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("failed to create upload session: You do not have the appropriate permissions.", err.Error())
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("invalid file", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := importUploadCmdF(s.th.Client, &cobra.Command{}, []string{"invalid_file"})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("failed to open import file: open invalid_file: no such file or directory", err.Error())
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("full upload", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
if c == s.th.LocalClient {
|
||||
cmd.Flags().Bool("local", true, "")
|
||||
}
|
||||
|
||||
err := importUploadCmdF(c, cmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(importName, printer.GetLines()[0].(*model.UploadSession).Filename)
|
||||
s.Require().Equal(importName, printer.GetLines()[1].(*model.FileInfo).Name)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("resume upload", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
userID := "me"
|
||||
cmd := &cobra.Command{}
|
||||
if c == s.th.LocalClient {
|
||||
cmd.Flags().Bool("local", true, "")
|
||||
userID = "nouser"
|
||||
}
|
||||
|
||||
us, _, err := c.CreateUpload(context.TODO(), &model.UploadSession{
|
||||
Filename: importName,
|
||||
FileSize: info.Size(),
|
||||
Type: model.UploadTypeImport,
|
||||
UserId: userID,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd.Flags().Bool("resume", true, "")
|
||||
cmd.Flags().String("upload", us.Id, "")
|
||||
|
||||
err = importUploadCmdF(c, cmd, []string{importFilePath})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(importName, printer.GetLines()[0].(*model.FileInfo).Name)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestImportProcessCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
importName := "import_test.zip"
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "tests", "import_test.zip")
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := importProcessCmdF(s.th.Client, &cobra.Command{}, []string{"importName"})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("failed to create import process job: You do not have the appropriate permissions.", err.Error())
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("process file", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
if c == s.th.LocalClient {
|
||||
cmd.Flags().Bool("local", true, "")
|
||||
}
|
||||
|
||||
err := importUploadCmdF(c, cmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
us := printer.GetLines()[0].(*model.UploadSession)
|
||||
printer.Clean()
|
||||
|
||||
err = importProcessCmdF(c, cmd, []string{us.Id + "_" + importName})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(us.Id+"_"+importName, printer.GetLines()[0].(*model.Job).Data["import_file"])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestImportListAvailableCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
importName := "import_test.zip"
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "tests", importName)
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := importListAvailableCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, "failed to list imports: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("no imports", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := importListAvailableCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Equal("No import files found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("some imports", func(c client.Client) {
|
||||
cmd := &cobra.Command{}
|
||||
if c == s.th.LocalClient {
|
||||
cmd.Flags().Bool("local", true, "")
|
||||
}
|
||||
|
||||
numImports := 3
|
||||
for range numImports {
|
||||
err := importUploadCmdF(c, cmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
printer.Clean()
|
||||
|
||||
imports, appErr := s.th.App.ListImports()
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := importListAvailableCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), len(imports))
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
for i, name := range printer.GetLines() {
|
||||
s.Require().Equal(imports[i], name.(string))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestImportListIncompleteCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForAllClients("no incomplete import uploads", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := importListIncompleteCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Equal("No incomplete import uploads found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("some incomplete import uploads", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
userID := "nouser"
|
||||
if c == s.th.SystemAdminClient {
|
||||
user, _, err := s.th.SystemAdminClient.GetMe(context.Background(), "")
|
||||
s.Require().NoError(err)
|
||||
userID = user.Id
|
||||
} else {
|
||||
cmd.Flags().Bool("local", true, "")
|
||||
}
|
||||
|
||||
us1, appErr := s.th.App.CreateUploadSession(s.th.Context, &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
UserId: userID,
|
||||
Type: model.UploadTypeImport,
|
||||
Filename: "import1.zip",
|
||||
FileSize: 1024 * 1024,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
us1.Path = ""
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
_, appErr = s.th.App.CreateUploadSession(s.th.Context, &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
UserId: userID,
|
||||
ChannelId: s.th.BasicChannel.Id,
|
||||
Type: model.UploadTypeAttachment,
|
||||
Filename: "somefile",
|
||||
FileSize: 1024 * 1024,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
us3, appErr := s.th.App.CreateUploadSession(s.th.Context, &model.UploadSession{
|
||||
Id: model.NewId(),
|
||||
UserId: userID,
|
||||
Type: model.UploadTypeImport,
|
||||
Filename: "import2.zip",
|
||||
FileSize: 1024 * 1024,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
us3.Path = ""
|
||||
|
||||
err := importListIncompleteCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(us1, printer.GetLines()[0].(*model.UploadSession))
|
||||
s.Require().Equal(us3, printer.GetLines()[1].(*model.UploadSession))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestImportJobShowCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: map[string]string{"import_file": "import1.zip"},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
job1, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: map[string]string{"import_file": "import1.zip"},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := importJobShowCmdF(s.th.Client, &cobra.Command{}, []string{job1.Id})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, "failed to get import job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("not found", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := importJobShowCmdF(c, &cobra.Command{}, []string{model.NewId()})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, "failed to get import job: Unable to get the job.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("found", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := importJobShowCmdF(c, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(job, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestImportJobListCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
err := importJobListCmdF(s.th.Client, cmd, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, "failed to get jobs: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("no import jobs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
err := importJobListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("some import jobs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 2
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
_, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: map[string]string{"import_file": "import1.zip"},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job2, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: map[string]string{"import_file": "import2.zip"},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job3, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: map[string]string{"import_file": "import3.zip"},
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := importJobListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), perPage)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(job3, printer.GetLines()[0].(*model.Job))
|
||||
s.Require().Equal(job2, printer.GetLines()[1].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestImportValidateCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
importName := "import_test.zip"
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "tests", importName)
|
||||
|
||||
s.RunForSystemAdminAndLocal("defaults", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringArray("team", nil, "")
|
||||
cmd.Flags().Bool("check-missing-teams", false, "")
|
||||
cmd.Flags().Bool("ignore-attachments", false, "")
|
||||
cmd.Flags().Bool("check-server-duplicates", true, "")
|
||||
|
||||
err := importValidateCmdF(c, cmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
s.Require().Equal(Statistics{
|
||||
Teams: 2,
|
||||
Channels: 24,
|
||||
Users: 16,
|
||||
Posts: 2001,
|
||||
DirectChannels: 79,
|
||||
DirectPosts: 901,
|
||||
Attachments: 2,
|
||||
}, printer.GetLines()[0].(Statistics))
|
||||
s.Require().Equal(struct {
|
||||
UnusedAttachments []string `json:"unused_attachments"`
|
||||
}{
|
||||
UnusedAttachments: []string{"data/test2.png", "data/test_img_diff_A.png", "data/test_img_diff_B.png"},
|
||||
}, printer.GetLines()[1].(struct {
|
||||
UnusedAttachments []string `json:"unused_attachments"`
|
||||
}))
|
||||
s.Require().Equal("Validation complete\n", printer.GetLines()[3])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("ignore attachments", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringArray("team", nil, "")
|
||||
cmd.Flags().Bool("check-missing-teams", false, "")
|
||||
cmd.Flags().Bool("ignore-attachments", true, "")
|
||||
cmd.Flags().Bool("check-server-duplicates", true, "")
|
||||
|
||||
err := importValidateCmdF(c, cmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
s.Require().Equal(Statistics{
|
||||
Teams: 2,
|
||||
Channels: 24,
|
||||
Users: 16,
|
||||
Posts: 2001,
|
||||
DirectChannels: 79,
|
||||
DirectPosts: 901,
|
||||
Attachments: 0,
|
||||
}, printer.GetLines()[0].(Statistics))
|
||||
s.Require().Equal("Validation complete\n", printer.GetLines()[2])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestImportDeleteCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := importDeleteCmdF(s.th.Client, &cobra.Command{}, []string{"import1.zip"})
|
||||
s.Require().EqualError(err, "failed to delete import: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("delete import", func(c client.Client) {
|
||||
importName := "import_test.zip"
|
||||
importFilePath := filepath.Join(server.GetPackagePath(), "tests", importName)
|
||||
importPath, err := filepath.Abs(filepath.Join(*s.th.App.Config().FileSettings.Directory,
|
||||
*s.th.App.Config().ImportSettings.Directory))
|
||||
s.Require().Nil(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
newImportName := "new_import_test.zip"
|
||||
err = utils.CopyFile(importFilePath, filepath.Join(importPath, newImportName))
|
||||
s.Require().Nil(err)
|
||||
|
||||
printer.Clean()
|
||||
imports, appErr := s.th.App.ListImports()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().NotEmpty(imports)
|
||||
s.Require().Equal(newImportName, imports[0])
|
||||
|
||||
err = importDeleteCmdF(c, cmd, []string{newImportName})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Equal(fmt.Sprintf(`Import file "%s" has been deleted`, newImportName), printer.GetLines()[0])
|
||||
|
||||
imports, appErr = s.th.App.ListImports()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Empty(imports)
|
||||
|
||||
//idempotency check
|
||||
|
||||
err = importDeleteCmdF(c, cmd, []string{newImportName})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Equal(fmt.Sprintf(`Import file "%s" has been deleted`, newImportName), printer.GetLines()[0])
|
||||
})
|
||||
}
|
||||
619
server/cmd/mmctl/commands/import_test.go
Normal file
619
server/cmd/mmctl/commands/import_test.go
Normal file
@@ -0,0 +1,619 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestImportListAvailableCmdF() {
|
||||
s.Run("no imports", func() {
|
||||
printer.Clean()
|
||||
var mockImports []string
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListImports(context.TODO()).
|
||||
Return(mockImports, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := importListAvailableCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Equal("No import files found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("some imports", func() {
|
||||
printer.Clean()
|
||||
mockImports := []string{
|
||||
"import1.zip",
|
||||
"import2.zip",
|
||||
"import3.zip",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListImports(context.TODO()).
|
||||
Return(mockImports, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := importListAvailableCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), len(mockImports))
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
for i, line := range printer.GetLines() {
|
||||
s.Equal(mockImports[i], line)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestImportListIncompleteCmdF() {
|
||||
s.Run("no incomplete uploads", func() {
|
||||
printer.Clean()
|
||||
var mockUploads []*model.UploadSession
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUploadsForUser(context.TODO(), "me").
|
||||
Return(mockUploads, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := importListIncompleteCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal("No incomplete import uploads found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("some incomplete uploads", func() {
|
||||
printer.Clean()
|
||||
mockUploads := []*model.UploadSession{
|
||||
{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeImport,
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeAttachment,
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
Type: model.UploadTypeImport,
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUploadsForUser(context.TODO(), "me").
|
||||
Return(mockUploads, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := importListIncompleteCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 2)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockUploads[0], printer.GetLines()[0].(*model.UploadSession))
|
||||
s.Equal(mockUploads[2], printer.GetLines()[1].(*model.UploadSession))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestImportJobShowCmdF() {
|
||||
s.Run("not found", func() {
|
||||
printer.Clean()
|
||||
|
||||
jobID := model.NewId()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJob(context.TODO(), jobID).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusNotFound}, errors.New("not found")).
|
||||
Times(1)
|
||||
|
||||
err := importJobShowCmdF(s.client, &cobra.Command{}, []string{jobID})
|
||||
s.Require().NotNil(err)
|
||||
s.Empty(printer.GetLines())
|
||||
s.Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.Run("found", func() {
|
||||
printer.Clean()
|
||||
mockJob := &model.Job{
|
||||
Id: model.NewId(),
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJob(context.TODO(), mockJob.Id).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := importJobShowCmdF(s.client, &cobra.Command{}, []string{mockJob.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestImportJobListCmdF() {
|
||||
s.Run("no import jobs", func() {
|
||||
printer.Clean()
|
||||
var mockJobs []*model.Job
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 10
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), model.JobTypeImportProcess, "", 0, perPage).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := importJobListCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("some import jobs", func() {
|
||||
printer.Clean()
|
||||
mockJobs := []*model.Job{
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 3
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), model.JobTypeImportProcess, "", 0, perPage).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := importJobListCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), len(mockJobs))
|
||||
s.Empty(printer.GetErrorLines())
|
||||
for i, line := range printer.GetLines() {
|
||||
s.Equal(mockJobs[i], line.(*model.Job))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestImportProcessCmdF() {
|
||||
s.Run("default workers", func() {
|
||||
printer.Clean()
|
||||
importFile := "import.zip"
|
||||
mockJob := &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: map[string]string{
|
||||
"import_file": importFile,
|
||||
"local_mode": "false",
|
||||
"extract_content": "false",
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateJob(context.TODO(), mockJob).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("bypass-upload", false, "")
|
||||
cmd.Flags().Bool("extract-content", false, "")
|
||||
cmd.Flags().Int("workers", 0, "")
|
||||
|
||||
err := importProcessCmdF(s.client, cmd, []string{importFile})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
|
||||
s.Run("workers exceeds max", func() {
|
||||
printer.Clean()
|
||||
importFile := "import.zip"
|
||||
tooMany := runtime.NumCPU()*4 + 1
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("bypass-upload", false, "")
|
||||
cmd.Flags().Bool("extract-content", false, "")
|
||||
cmd.Flags().Int("workers", 0, "")
|
||||
_ = cmd.Flags().Set("workers", strconv.Itoa(tooMany))
|
||||
|
||||
err := importProcessCmdF(s.client, cmd, []string{importFile})
|
||||
s.Require().NotNil(err)
|
||||
s.Contains(err.Error(), "exceeds maximum allowed")
|
||||
s.Empty(printer.GetLines())
|
||||
s.Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.Run("custom workers", func() {
|
||||
printer.Clean()
|
||||
importFile := "import.zip"
|
||||
mockJob := &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: map[string]string{
|
||||
"import_file": importFile,
|
||||
"local_mode": "false",
|
||||
"extract_content": "false",
|
||||
"workers": "2",
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateJob(context.TODO(), mockJob).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("bypass-upload", false, "")
|
||||
cmd.Flags().Bool("extract-content", false, "")
|
||||
cmd.Flags().Int("workers", 0, "")
|
||||
_ = cmd.Flags().Set("workers", "2")
|
||||
|
||||
err := importProcessCmdF(s.client, cmd, []string{importFile})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestImportValidateCmdF() {
|
||||
importFilePath := filepath.Join(os.TempDir(), "import.zip")
|
||||
|
||||
importBase := `{"type":"version","version":1}
|
||||
{"type":"team","team":{"name":"reiciendis-0","display_name":"minus","type":"O","description":"doloremque dignissimos velit eum quae non omnis. dolores rerum cupiditate porro quia aperiam necessitatibus natus aut. velit eveniet porro explicabo tempora voluptas beatae. eum saepe a aut. perferendis aut ab ipsum! molestias animi ut porro dolores vel. ","allow_open_invite":false}}
|
||||
{"type":"team","team":{"name":"ad-1","display_name":"eligendi","type":"O","description":"et iste illum reprehenderit aliquid in rem itaque in maxime eius.","allow_open_invite":false}}
|
||||
{"type":"channel","channel":{"team":"ad-1","name":"iusto-9","display_name":"incidunt","type":"P","header":"officia accusamus aut aliquid dolor qui. quia magni pariatur numquam nesciunt. maxime dolorum sit neque commodi dolorum qui dicta sit. labore laudantium quisquam voluptatem commodi magnam. est aliquid perspiciatis sequi adipisci modi sit nam. totam iste quidem sed mollitia earum. vel voluptates labore cumque eaque qui!","purpose":"sit et accusamus repudiandae id. ut et officiis eos quod. sit soluta aliquid pariatur consectetur nostrum aut magni. numquam quas aspernatur et voluptatum et ipsam animi."}}
|
||||
{"type":"user","user":{"username":"ashley.berry","email":"user-12@sample.mattermost.com","auth_service":null,"nickname":"","first_name":"Ashley","last_name":"Berry","position":"Registered Nurse","roles":"system_user","locale":"en","delete_at":0,"teams":[{"name":"reiciendis-0","roles":"team_admin team_user","channels":[{"name":"town-square","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"doloremque-0","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"voluptas-9","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"minus-8","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"rem-7","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"odit-3","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":true}]},{"name":"ad-1","roles":"team_user","channels":[{"name":"iusto-9","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"amet-0","roles":"channel_admin channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"minus-6","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"autem-2","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"town-square","roles":"channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false},{"name":"aut-8","roles":"channel_admin channel_user","notify_props":{"desktop":"default","mobile":"default","mark_unread":"all"},"favorite":false}]}],"military_time":"false","link_previews":"true","message_display":"compact","channel_display_mode":"full","tutorial_step":"2","notify_props":{"desktop":"mention","desktop_sound":"true","email":"true","mobile":"mention","mobile_push_status":"away","channel":"true","comments":"never","mention_keys":""}}}
|
||||
{"type":"direct_channel","direct_channel":{"members":["ashley.berry","ashley.berry"],"favorited_by":null,"header":""}}`
|
||||
|
||||
s.Run("empty file", func() {
|
||||
file, err := os.Create(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
zipWr := zip.NewWriter(file)
|
||||
wr, err := zipWr.Create("import.jsonl")
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = wr.Write([]byte(``))
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = zipWr.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = file.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
printer.Clean()
|
||||
err = importValidateCmdF(nil, ImportValidateCmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 5)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(Statistics{}, printer.GetLines()[2].(Statistics))
|
||||
s.Equal("Validation complete\n", printer.GetLines()[4])
|
||||
})
|
||||
|
||||
s.Run("post size under default limit", func() {
|
||||
file, err := os.Create(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
zipWr := zip.NewWriter(file)
|
||||
wr, err := zipWr.Create("import.jsonl")
|
||||
s.Require().NoError(err)
|
||||
|
||||
msg := strings.Repeat("t", model.PostMessageMaxRunesV2)
|
||||
|
||||
_, err = wr.Write([]byte(importBase))
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = wr.Write(fmt.Appendf(nil, `
|
||||
{"type":"post","post":{"team":"ad-1","channel":"iusto-9","user":"ashley.berry","message":"%s","props":{},"create_at":1603398068740,"reactions":null,"replies":null}}`, msg))
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = zipWr.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = file.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
printer.Clean()
|
||||
err = importValidateCmdF(nil, ImportValidateCmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(Statistics{
|
||||
Teams: 2,
|
||||
Channels: 1,
|
||||
DirectChannels: 1,
|
||||
Users: 1,
|
||||
Posts: 1,
|
||||
}, printer.GetLines()[0].(Statistics))
|
||||
res := printer.GetLines()[1].(ImportValidationResult)
|
||||
s.Require().Empty(res.Errors)
|
||||
s.Equal("Validation complete\n", printer.GetLines()[2])
|
||||
})
|
||||
|
||||
s.Run("post size above default limit", func() {
|
||||
file, err := os.Create(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
zipWr := zip.NewWriter(file)
|
||||
wr, err := zipWr.Create("import.jsonl")
|
||||
s.Require().NoError(err)
|
||||
|
||||
msg := strings.Repeat("t", model.PostMessageMaxRunesV2+1)
|
||||
|
||||
_, err = wr.Write([]byte(importBase))
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = wr.Write(fmt.Appendf(nil, `
|
||||
{"type":"post","post":{"team":"ad-1","channel":"iusto-9","user":"ashley.berry","message":"%s","props":{},"create_at":1603398068740,"reactions":null,"replies":null}}`, msg))
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = zipWr.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = file.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
printer.Clean()
|
||||
err = importValidateCmdF(nil, ImportValidateCmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(Statistics{
|
||||
Teams: 2,
|
||||
Channels: 1,
|
||||
DirectChannels: 1,
|
||||
Users: 1,
|
||||
Posts: 1,
|
||||
}, printer.GetLines()[0].(Statistics))
|
||||
res := printer.GetLines()[1].(ImportValidationResult)
|
||||
s.Require().Len(res.Errors, 1)
|
||||
|
||||
s.Require().Equal("app.import.validate_post_import_data.message_length.error", res.Errors[0].Err.(*model.AppError).Id)
|
||||
|
||||
s.Equal("Validation complete\n", printer.GetLines()[2])
|
||||
})
|
||||
|
||||
s.Run("post size below config limit", func() {
|
||||
file, err := os.Create(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
zipWr := zip.NewWriter(file)
|
||||
wr, err := zipWr.Create("import.jsonl")
|
||||
s.Require().NoError(err)
|
||||
|
||||
msg := strings.Repeat("t", model.PostMessageMaxRunesV2*2)
|
||||
|
||||
_, err = wr.Write([]byte(importBase))
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = wr.Write(fmt.Appendf(nil, `
|
||||
{"type":"post","post":{"team":"ad-1","channel":"iusto-9","user":"ashley.berry","message":"%s","props":{},"create_at":1603398068740,"reactions":null,"replies":null}}`, msg))
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = zipWr.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = file.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsers(context.TODO(), 0, 200, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetAllTeams(context.TODO(), "", 0, 200).
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetClientConfig(context.TODO(), "").
|
||||
Return(map[string]string{
|
||||
"MaxPostSize": fmt.Sprintf("%d", model.PostMessageMaxRunesV2*2),
|
||||
}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = importValidateCmdF(s.client, ImportValidateCmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(Statistics{
|
||||
Teams: 2,
|
||||
Channels: 1,
|
||||
DirectChannels: 1,
|
||||
Users: 1,
|
||||
Posts: 1,
|
||||
}, printer.GetLines()[0].(Statistics))
|
||||
res := printer.GetLines()[1].(ImportValidationResult)
|
||||
s.Require().Empty(res.Errors)
|
||||
s.Equal("Validation complete\n", printer.GetLines()[2])
|
||||
})
|
||||
|
||||
s.Run("direct post size below config limit", func() {
|
||||
file, err := os.Create(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
zipWr := zip.NewWriter(file)
|
||||
wr, err := zipWr.Create("import.jsonl")
|
||||
s.Require().NoError(err)
|
||||
|
||||
msg := strings.Repeat("t", model.PostMessageMaxRunesV2*2)
|
||||
|
||||
_, err = wr.Write([]byte(importBase))
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = wr.Write(fmt.Appendf(nil, `
|
||||
{"type":"direct_post","direct_post":{"channel_members":["ashley.berry","ashley.berry"],"user":"ashley.berry","message":"%s","props":{},"create_at":1603398112372,"flagged_by":null,"reactions":null,"replies":null,"attachments":null}}`, msg))
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = zipWr.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = file.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsers(context.TODO(), 0, 200, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetAllTeams(context.TODO(), "", 0, 200).
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetClientConfig(context.TODO(), "").
|
||||
Return(map[string]string{
|
||||
"MaxPostSize": fmt.Sprintf("%d", model.PostMessageMaxRunesV2*2),
|
||||
}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = importValidateCmdF(s.client, ImportValidateCmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(Statistics{
|
||||
Teams: 2,
|
||||
Channels: 1,
|
||||
Users: 1,
|
||||
DirectChannels: 1,
|
||||
DirectPosts: 1,
|
||||
}, printer.GetLines()[0].(Statistics))
|
||||
res := printer.GetLines()[1].(ImportValidationResult)
|
||||
s.Require().Empty(res.Errors)
|
||||
s.Equal("Validation complete\n", printer.GetLines()[2])
|
||||
})
|
||||
|
||||
s.Run("invalid file attachment path", func() {
|
||||
file, err := os.Create(importFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
zipWr := zip.NewWriter(file)
|
||||
wr, err := zipWr.Create("import.jsonl")
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = wr.Write([]byte(importBase))
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = wr.Write([]byte(`
|
||||
{"type":"post","post":{"team":"ad-1","channel":"iusto-9","user":"ashley.berry","message":"message","props":{},"create_at":1603398068740,"reactions":null,"replies":null,"attachments":[{"path": "data/../../invalid.jpg"}]}}`))
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = zipWr.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = file.Close()
|
||||
s.Require().NoError(err)
|
||||
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsers(context.TODO(), 0, 200, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetAllTeams(context.TODO(), "", 0, 200).
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetClientConfig(context.TODO(), "").
|
||||
Return(map[string]string{
|
||||
"MaxPostSize": fmt.Sprintf("%d", model.PostMessageMaxRunesV2*2),
|
||||
}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = importValidateCmdF(s.client, ImportValidateCmd, []string{importFilePath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(Statistics{
|
||||
Teams: 2,
|
||||
Channels: 1,
|
||||
DirectChannels: 1,
|
||||
Users: 1,
|
||||
Posts: 1,
|
||||
}, printer.GetLines()[0].(Statistics))
|
||||
res := printer.GetLines()[1].(ImportValidationResult)
|
||||
s.Require().Len(res.Errors, 2)
|
||||
s.Require().Equal("app.import.validate_post_import_data.attachment.error", res.Errors[0].Err.(*model.AppError).Id)
|
||||
s.Equal("Validation complete\n", printer.GetLines()[2])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestDeleteImportCmdF() {
|
||||
s.Run("delete command succeeds", func() {
|
||||
printer.Clean()
|
||||
s.client.
|
||||
EXPECT().
|
||||
DeleteImport(context.TODO(), "import.zip").
|
||||
Return(&model.Response{}, nil).
|
||||
Times(2)
|
||||
|
||||
err := importDeleteCmdF(s.client, &cobra.Command{}, []string{"import.zip"})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Equal("Import file \"import.zip\" has been deleted", printer.GetLines()[0])
|
||||
|
||||
//idempotency check
|
||||
err = importDeleteCmdF(s.client, &cobra.Command{}, []string{"import.zip"})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 2)
|
||||
s.Equal("Import file \"import.zip\" has been deleted", printer.GetLines()[1])
|
||||
})
|
||||
}
|
||||
66
server/cmd/mmctl/commands/importer/utils.go
Normal file
66
server/cmd/mmctl/commands/importer/utils.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package importer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ImportFileInfo struct {
|
||||
Source string `json:"archive_name"`
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
CurrentLine uint64 `json:"current_line,omitempty"`
|
||||
TotalLines uint64 `json:"total_lines,omitempty"`
|
||||
}
|
||||
|
||||
type ImportValidationError struct { //nolint:govet
|
||||
ImportFileInfo
|
||||
FieldName string
|
||||
Err error
|
||||
Suggestion string
|
||||
SuggestedValues []any
|
||||
ApplySuggestion func(any) error
|
||||
}
|
||||
|
||||
func (e *ImportValidationError) MarshalJSON() ([]byte, error) {
|
||||
t := struct { //nolint:govet
|
||||
ImportFileInfo
|
||||
FieldName string `json:"field_name,omitempty"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Suggestion string `json:"suggestion,omitempty"`
|
||||
SuggestedValues []any `json:"suggested_values,omitempty"`
|
||||
}{
|
||||
ImportFileInfo: e.ImportFileInfo,
|
||||
FieldName: e.FieldName,
|
||||
Suggestion: e.Suggestion,
|
||||
SuggestedValues: e.SuggestedValues,
|
||||
}
|
||||
|
||||
if e.Err != nil {
|
||||
t.Err = e.Err.Error()
|
||||
}
|
||||
|
||||
return json.Marshal(t)
|
||||
}
|
||||
|
||||
func (e *ImportValidationError) Error() string {
|
||||
msg := &strings.Builder{}
|
||||
msg.WriteString("import validation error")
|
||||
|
||||
if e.FileName != "" || e.Source != "" {
|
||||
fmt.Fprintf(msg, " in %s->%s:%d", e.Source, e.FileName, e.CurrentLine)
|
||||
}
|
||||
|
||||
if e.FieldName != "" {
|
||||
fmt.Fprintf(msg, " field %q", e.FieldName)
|
||||
}
|
||||
|
||||
if e.Err != nil {
|
||||
fmt.Fprintf(msg, ": %s", e.Err)
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
}
|
||||
1145
server/cmd/mmctl/commands/importer/validate.go
Normal file
1145
server/cmd/mmctl/commands/importer/validate.go
Normal file
File diff suppressed because it is too large
Load Diff
267
server/cmd/mmctl/commands/init.go
Normal file
267
server/cmd/mmctl/commands/init.go
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
const (
|
||||
shellCompletionMaxItems = 50 // Maximum number of items that will be loaded and shown in shell completion.
|
||||
shellCompleteTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
insecureSignatureAlgorithms = map[x509.SignatureAlgorithm]bool{
|
||||
x509.SHA1WithRSA: true,
|
||||
x509.DSAWithSHA1: true,
|
||||
x509.ECDSAWithSHA1: true,
|
||||
}
|
||||
expectedSocketMode = os.ModeSocket | 0600
|
||||
)
|
||||
|
||||
func CheckVersionMatch(version, serverVersion string) (bool, error) {
|
||||
mmctlVersionParsed, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse version range %s", version)
|
||||
}
|
||||
|
||||
// Split and recombine the server version string
|
||||
parts := strings.Split(serverVersion, ".")
|
||||
if len(parts) < 3 {
|
||||
return false, fmt.Errorf("incorrect server version format: %s", serverVersion)
|
||||
}
|
||||
serverVersion = strings.Join(parts[:3], ".")
|
||||
|
||||
serverVersionParsed, err := semver.NewVersion(serverVersion)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse version range %s", serverVersion)
|
||||
}
|
||||
|
||||
if serverVersionParsed.Major() != mmctlVersionParsed.Major() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if mmctlVersionParsed.Minor() > serverVersionParsed.Minor() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func getClient(ctx context.Context, cmd *cobra.Command) (*model.Client4, string, bool, error) {
|
||||
useLocal := viper.GetBool("local")
|
||||
|
||||
if !useLocal {
|
||||
// Assume local mode if no server address is provided
|
||||
credentials, err := GetCurrentCredentials()
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Warning: Unable to retrieve credentials, assuming --local mode")
|
||||
useLocal = true
|
||||
} else if credentials == nil {
|
||||
cmd.PrintErrln("Warning: No credentials found, assuming --local mode")
|
||||
useLocal = true
|
||||
}
|
||||
}
|
||||
|
||||
if useLocal {
|
||||
c, err := InitUnixClient(viper.GetString("local-socket-path"))
|
||||
if err != nil {
|
||||
return nil, "", true, err
|
||||
}
|
||||
printer.SetServerAddres("local instance")
|
||||
|
||||
return c, "", true, nil
|
||||
}
|
||||
|
||||
c, serverVersion, err := InitClient(ctx, viper.GetBool("insecure-sha1-intermediate"), viper.GetBool("insecure-tls-version"))
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
|
||||
return c, serverVersion, false, nil
|
||||
}
|
||||
|
||||
func withClient(fn func(c client.Client, cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.TODO()
|
||||
c, serverVersion, local, err := getClient(ctx, cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
if local {
|
||||
return fn(c, cmd, args)
|
||||
}
|
||||
|
||||
if Version != "unspecified" { // unspecified version indicates that we are on dev mode.
|
||||
valid, err := CheckVersionMatch(Version, serverVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not check version mismatch: %w", err)
|
||||
}
|
||||
if !valid {
|
||||
if viper.GetBool("strict") {
|
||||
printer.PrintError("ERROR: server version " + serverVersion + " doesn't match with mmctl version " + Version + ". Strict flag is set, so the command will not be run")
|
||||
os.Exit(1)
|
||||
}
|
||||
if !viper.GetBool("suppress-warnings") {
|
||||
printer.PrintWarning("server version " + serverVersion + " doesn't match mmctl version " + Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printer.SetServerAddres(c.APIURL)
|
||||
return fn(c, cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
func localOnlyPrecheck(cmd *cobra.Command, args []string) {
|
||||
local := viper.GetBool("local")
|
||||
if !local {
|
||||
fmt.Fprintln(os.Stderr, "This command can only be run in local mode")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func disableLocalPrecheck(cmd *cobra.Command, args []string) {
|
||||
local := viper.GetBool("local")
|
||||
if local {
|
||||
fmt.Fprintln(os.Stderr, "This command cannot be run in local mode")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func isValidChain(chain []*x509.Certificate) bool {
|
||||
// check all certs but the root one
|
||||
certs := chain[:len(chain)-1]
|
||||
|
||||
for _, cert := range certs {
|
||||
if _, ok := insecureSignatureAlgorithms[cert.SignatureAlgorithm]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func VerifyCertificates(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
// loop over certificate chains
|
||||
if slices.ContainsFunc(verifiedChains, isValidChain) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("insecure algorithm found in the certificate chain. Use --insecure-sha1-intermediate flag to ignore. Aborting")
|
||||
}
|
||||
|
||||
func NewAPIv4Client(instanceURL string, allowInsecureSHA1, allowInsecureTLS bool) *model.Client4 {
|
||||
client := model.NewAPIv4Client(instanceURL)
|
||||
userAgent := fmt.Sprintf("mmctl/%s (%s)", Version, runtime.GOOS)
|
||||
client.HTTPHeader = map[string]string{"User-Agent": userAgent}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
if allowInsecureTLS {
|
||||
tlsConfig.MinVersion = tls.VersionTLS10
|
||||
}
|
||||
|
||||
if !allowInsecureSHA1 {
|
||||
tlsConfig.VerifyPeerCertificate = VerifyCertificates
|
||||
}
|
||||
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func InitClientWithUsernameAndPassword(ctx context.Context, username, password, instanceURL string, allowInsecureSHA1, allowInsecureTLS bool) (*model.Client4, string, error) {
|
||||
client := NewAPIv4Client(instanceURL, allowInsecureSHA1, allowInsecureTLS)
|
||||
|
||||
_, resp, err := client.Login(ctx, username, password)
|
||||
if err != nil {
|
||||
return nil, "", checkInsecureTLSError(err, allowInsecureTLS)
|
||||
}
|
||||
return client, resp.ServerVersion, nil
|
||||
}
|
||||
|
||||
func InitClientWithMFA(ctx context.Context, username, password, mfaToken, instanceURL string, allowInsecureSHA1, allowInsecureTLS bool) (*model.Client4, string, error) {
|
||||
client := NewAPIv4Client(instanceURL, allowInsecureSHA1, allowInsecureTLS)
|
||||
_, resp, err := client.LoginWithMFA(ctx, username, password, mfaToken)
|
||||
if err != nil {
|
||||
return nil, "", checkInsecureTLSError(err, allowInsecureTLS)
|
||||
}
|
||||
return client, resp.ServerVersion, nil
|
||||
}
|
||||
|
||||
func InitClientWithCredentials(ctx context.Context, credentials *Credentials, allowInsecureSHA1, allowInsecureTLS bool) (*model.Client4, string, error) {
|
||||
client := NewAPIv4Client(credentials.InstanceURL, allowInsecureSHA1, allowInsecureTLS)
|
||||
|
||||
client.AuthType = model.HeaderBearer
|
||||
client.AuthToken = credentials.AuthToken
|
||||
|
||||
_, resp, err := client.GetMe(ctx, "")
|
||||
if err != nil {
|
||||
return nil, "", checkInsecureTLSError(err, allowInsecureTLS)
|
||||
}
|
||||
|
||||
return client, resp.ServerVersion, nil
|
||||
}
|
||||
|
||||
func InitClient(ctx context.Context, allowInsecureSHA1, allowInsecureTLS bool) (*model.Client4, string, error) {
|
||||
credentials, err := GetCurrentCredentials()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return InitClientWithCredentials(ctx, credentials, allowInsecureSHA1, allowInsecureTLS)
|
||||
}
|
||||
|
||||
func InitWebSocketClient() (*model.WebSocketClient, error) {
|
||||
credentials, err := GetCurrentCredentials()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, appErr := model.NewWebSocketClient4(strings.Replace(credentials.InstanceURL, "http", "ws", 1), credentials.AuthToken)
|
||||
if appErr != nil {
|
||||
return nil, errors.Wrap(appErr, "unable to create the websockets connection")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func InitUnixClient(socketPath string) (*model.Client4, error) {
|
||||
if err := checkValidSocket(socketPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return model.NewAPIv4SocketClient(socketPath), nil
|
||||
}
|
||||
|
||||
func checkInsecureTLSError(err error, allowInsecureTLS bool) error {
|
||||
if (strings.Contains(err.Error(), "tls: protocol version not supported") ||
|
||||
strings.Contains(err.Error(), "tls: server selected unsupported protocol version")) && !allowInsecureTLS {
|
||||
return errors.New("won't perform action through an insecure TLS connection. Please add --insecure-tls-version to bypass this check")
|
||||
}
|
||||
return err
|
||||
}
|
||||
194
server/cmd/mmctl/commands/init_test.go
Normal file
194
server/cmd/mmctl/commands/init_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCheckVersionMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Version string
|
||||
ServerVersion string
|
||||
Expected bool
|
||||
ErrExpected bool
|
||||
}{
|
||||
{
|
||||
Name: "Both versions are equal",
|
||||
Version: "1.2.3",
|
||||
ServerVersion: "1.2.3.dev.993e5a2cb546b0116ecaae1862b6a1c6.true",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "Only patch version is different",
|
||||
Version: "1.2.3",
|
||||
ServerVersion: "1.2.7.dev.993e5a2cb546b0116ecaae1862b6a1c6.false",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "Major version is greater",
|
||||
Version: "1.2.3",
|
||||
ServerVersion: "7.0.0.7.0.0.8215d92df0b8458789408eb07ccfdaae.false",
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "Major version is less",
|
||||
Version: "8.2.3",
|
||||
ServerVersion: "7.0.0.7.0.0.8215d92df0b8458789408eb07ccfdaae.false",
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "Minor version is greater",
|
||||
Version: "1.2.3",
|
||||
ServerVersion: "1.3.3.1.3.3.8215d92df0b8458789408eb07ccfdaae.true",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "Minor version is less",
|
||||
Version: "1.2.3",
|
||||
ServerVersion: "1.1.3.1.1.3.8215d92df0b8458789408eb07ccfdaae.false",
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "Both versions are equal but one has v in front of it",
|
||||
Version: "v1.2.3",
|
||||
ServerVersion: "1.2.3.dev.8215d92df0b8458789408eb07ccfdaae.false",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "unspecified version",
|
||||
Version: "",
|
||||
ServerVersion: "1.2.3",
|
||||
Expected: false,
|
||||
ErrExpected: true,
|
||||
},
|
||||
{
|
||||
Name: "bad version",
|
||||
Version: "1.2.3",
|
||||
ServerVersion: "1.2",
|
||||
Expected: false,
|
||||
ErrExpected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
res, err := CheckVersionMatch(tc.Version, tc.ServerVersion)
|
||||
require.True(t, (err != nil) == tc.ErrExpected)
|
||||
require.Equal(t, tc.Expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyCertificates(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Chains [][]*x509.Certificate
|
||||
ExpectedError bool
|
||||
}{
|
||||
{
|
||||
Name: "One chain with a root SHA1 cert",
|
||||
Chains: [][]*x509.Certificate{
|
||||
{
|
||||
{SignatureAlgorithm: x509.SHA256WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA1WithRSA},
|
||||
},
|
||||
},
|
||||
ExpectedError: false,
|
||||
},
|
||||
{
|
||||
Name: "One chain with an intermediate SHA1 cert",
|
||||
Chains: [][]*x509.Certificate{
|
||||
{
|
||||
{SignatureAlgorithm: x509.SHA256WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA1WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA1WithRSA},
|
||||
},
|
||||
},
|
||||
ExpectedError: true,
|
||||
},
|
||||
{
|
||||
Name: "One valid chain and other invalid",
|
||||
Chains: [][]*x509.Certificate{
|
||||
{
|
||||
{SignatureAlgorithm: x509.SHA256WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA1WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA1WithRSA},
|
||||
},
|
||||
{
|
||||
{SignatureAlgorithm: x509.SHA256WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA256WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA1WithRSA},
|
||||
},
|
||||
},
|
||||
ExpectedError: false,
|
||||
},
|
||||
{
|
||||
Name: "Two invalid chains",
|
||||
Chains: [][]*x509.Certificate{
|
||||
{
|
||||
{SignatureAlgorithm: x509.SHA256WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA1WithRSA},
|
||||
{SignatureAlgorithm: x509.SHA1WithRSA},
|
||||
},
|
||||
{
|
||||
{SignatureAlgorithm: x509.SHA256WithRSA},
|
||||
{SignatureAlgorithm: x509.DSAWithSHA1},
|
||||
{SignatureAlgorithm: x509.DSAWithSHA1},
|
||||
},
|
||||
},
|
||||
ExpectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
err := VerifyCertificates([][]byte{}, tc.Chains)
|
||||
if tc.ExpectedError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAPIv4Client(t *testing.T) {
|
||||
t.Run("should take http proxy into account", func(t *testing.T) {
|
||||
router := mux.NewRouter()
|
||||
router.Handle("/api/v4/users/me", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
}
|
||||
err := json.NewEncoder(w).Encode(user)
|
||||
require.NoError(t, err)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
s := httptest.NewServer(router)
|
||||
defer s.Close()
|
||||
|
||||
proxyAddr := s.Listener.Addr().String()
|
||||
_, port, err := net.SplitHostPort(proxyAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// t.Setenv prevents t.Parallel — env var has no config equivalent
|
||||
t.Setenv("HTTP_PROXY", proxyAddr)
|
||||
|
||||
client := NewAPIv4Client("http://somethingelse:"+port, false, false)
|
||||
_, _, err = client.GetMe(context.Background(), "")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
104
server/cmd/mmctl/commands/integrity.go
Normal file
104
server/cmd/mmctl/commands/integrity.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var IntegrityCmd = &cobra.Command{
|
||||
Use: "integrity",
|
||||
Short: "Check database records integrity.",
|
||||
Long: "Perform a relational integrity check which returns information about any orphaned record found.",
|
||||
Args: cobra.NoArgs,
|
||||
PreRun: localOnlyPrecheck,
|
||||
RunE: withClient(integrityCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
IntegrityCmd.Flags().Bool("confirm", false, "Confirm you really want to run a complete integrity check that may temporarily harm system performance")
|
||||
IntegrityCmd.Flags().BoolP("verbose", "v", false, "Show detailed information on integrity check results")
|
||||
RootCmd.AddCommand(IntegrityCmd)
|
||||
}
|
||||
|
||||
func printRelationalIntegrityCheckResult(data model.RelationalIntegrityCheckData, verbose bool) {
|
||||
printer.PrintT("Found {{len .Records}} in relation {{ .ChildName }} orphans of relation {{ .ParentName }}", data)
|
||||
if !verbose {
|
||||
return
|
||||
}
|
||||
const null = "NULL"
|
||||
const empty = "empty"
|
||||
for _, record := range data.Records {
|
||||
var parentID string
|
||||
|
||||
switch {
|
||||
case record.ParentId == nil:
|
||||
parentID = null
|
||||
case *record.ParentId == "":
|
||||
parentID = empty
|
||||
default:
|
||||
parentID = *record.ParentId
|
||||
}
|
||||
|
||||
if record.ChildId != nil {
|
||||
if parentID == null || parentID == empty {
|
||||
fmt.Printf(" Child %s (%s.%s) has %s ParentIdAttr (%s.%s)\n", *record.ChildId, data.ChildName, data.ChildIdAttr, parentID, data.ChildName, data.ParentIdAttr)
|
||||
} else {
|
||||
fmt.Printf(" Child %s (%s.%s) is missing Parent %s (%s.%s)\n", *record.ChildId, data.ChildName, data.ChildIdAttr, parentID, data.ChildName, data.ParentIdAttr)
|
||||
}
|
||||
} else {
|
||||
if parentID == null || parentID == empty {
|
||||
fmt.Printf(" Child has %s ParentIdAttr (%s.%s)\n", parentID, data.ChildName, data.ParentIdAttr)
|
||||
} else {
|
||||
fmt.Printf(" Child is missing Parent %s (%s.%s)\n", parentID, data.ChildName, data.ParentIdAttr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printIntegrityCheckResult(result model.IntegrityCheckResult, verbose bool) {
|
||||
switch data := result.Data.(type) {
|
||||
case model.RelationalIntegrityCheckData:
|
||||
printRelationalIntegrityCheckResult(data, verbose)
|
||||
default:
|
||||
printer.PrintError("invalid data type")
|
||||
}
|
||||
}
|
||||
|
||||
func integrityCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
confirmFlag, _ := command.Flags().GetBool("confirm")
|
||||
if !confirmFlag {
|
||||
if err := getConfirmation("This check may harm performance on live systems. Are you sure you want to proceed?", false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
verboseFlag, _ := command.Flags().GetBool("verbose")
|
||||
|
||||
results, _, err := c.CheckIntegrity(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to perform integrity check. Error: %w", err)
|
||||
}
|
||||
|
||||
var errs *multierror.Error
|
||||
for _, result := range results {
|
||||
if result.Err != nil {
|
||||
errs = multierror.Append(errs, result.Err)
|
||||
printer.PrintError(result.Err.Error())
|
||||
continue
|
||||
}
|
||||
printIntegrityCheckResult(result, verboseFlag)
|
||||
}
|
||||
|
||||
return errs.ErrorOrNil()
|
||||
}
|
||||
115
server/cmd/mmctl/commands/integrity_test.go
Normal file
115
server/cmd/mmctl/commands/integrity_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestIntegrityCmd() {
|
||||
s.Run("Integrity check succeeds", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
|
||||
mockData := model.RelationalIntegrityCheckData{
|
||||
ParentName: "parent",
|
||||
ChildName: "child",
|
||||
ParentIdAttr: "parentIdAttr",
|
||||
ChildIdAttr: "childIdAttr",
|
||||
Records: []model.OrphanedRecord{
|
||||
{
|
||||
ParentId: new("parentId"),
|
||||
ChildId: new("childId"),
|
||||
},
|
||||
},
|
||||
}
|
||||
mockResults := []model.IntegrityCheckResult{
|
||||
{
|
||||
Data: mockData,
|
||||
Err: nil,
|
||||
},
|
||||
}
|
||||
s.client.
|
||||
EXPECT().
|
||||
CheckIntegrity(context.TODO()).
|
||||
Return(mockResults, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := integrityCmdF(s.client, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(mockData, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Integrity check fails", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CheckIntegrity(context.TODO()).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := integrityCmdF(s.client, cmd, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal("unable to perform integrity check. Error: mock error", err.Error())
|
||||
})
|
||||
|
||||
s.Run("Integrity check with errors", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
|
||||
mockData := model.RelationalIntegrityCheckData{
|
||||
ParentName: "parent",
|
||||
ChildName: "child",
|
||||
ParentIdAttr: "parentIdAttr",
|
||||
ChildIdAttr: "childIdAttr",
|
||||
Records: []model.OrphanedRecord{
|
||||
{
|
||||
ParentId: new("parentId"),
|
||||
ChildId: new("childId"),
|
||||
},
|
||||
},
|
||||
}
|
||||
mockResults := []model.IntegrityCheckResult{
|
||||
{
|
||||
Data: nil,
|
||||
Err: errors.New("test error"),
|
||||
},
|
||||
{
|
||||
Data: mockData,
|
||||
Err: nil,
|
||||
},
|
||||
}
|
||||
s.client.
|
||||
EXPECT().
|
||||
CheckIntegrity(context.TODO()).
|
||||
Return(mockResults, &model.Response{}, nil).
|
||||
Times(1)
|
||||
var expected error
|
||||
expected = multierror.Append(expected, errors.New("test error"))
|
||||
|
||||
err := integrityCmdF(s.client, cmd, []string{})
|
||||
s.Require().EqualError(err, expected.Error())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(mockData, printer.GetLines()[0])
|
||||
s.Require().Equal("test error", printer.GetErrorLines()[0])
|
||||
})
|
||||
}
|
||||
303
server/cmd/mmctl/commands/job.go
Normal file
303
server/cmd/mmctl/commands/job.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var JobCmd = &cobra.Command{
|
||||
Use: "job",
|
||||
Short: "Management of jobs",
|
||||
}
|
||||
|
||||
var listJobsCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List the latest jobs",
|
||||
Example: ` job list
|
||||
job list --ids jobID1,jobID2
|
||||
job list --type ldap_sync --status success
|
||||
job list --type ldap_sync --status success --page 0 --per-page 10`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(listJobsCmdF),
|
||||
}
|
||||
|
||||
var createJobCmd = &cobra.Command{
|
||||
Use: "create [type]",
|
||||
Short: "Create a new job",
|
||||
Long: "Create a new job of the given type. Use --data to pass type-specific options as key=value pairs.",
|
||||
Example: ` job create ldap_sync
|
||||
job create data_retention
|
||||
job create message_export --data batch_size=1000`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: jobTypeCompletionF,
|
||||
RunE: withClient(createJobCmdF),
|
||||
}
|
||||
|
||||
var showJobCmd = &cobra.Command{
|
||||
Use: "show [job]",
|
||||
Short: "Show a job",
|
||||
Example: " job show jobID",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: validateArgsWithClient(jobCompletionF),
|
||||
RunE: withClient(showJobCmdF),
|
||||
}
|
||||
|
||||
var cancelJobCmd = &cobra.Command{
|
||||
Use: "cancel [job]",
|
||||
Short: "Cancel a job",
|
||||
Example: " job cancel jobID",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: validateArgsWithClient(jobCompletionF),
|
||||
RunE: withClient(cancelJobCmdF),
|
||||
}
|
||||
|
||||
var updateJobCmd = &cobra.Command{
|
||||
Use: "update [job] [status]",
|
||||
Short: "Update the status of a job",
|
||||
Long: `Update the status of a job. The following restrictions are in place:
|
||||
- in_progress -> pending
|
||||
- in_progress | pending -> cancel_requested
|
||||
- cancel_requested -> canceled
|
||||
|
||||
Those restriction can be bypassed with --force=true but the only statuses you can go to are: pending, cancel_requested and canceled. This can have unexpected consequences and should be used with caution.`,
|
||||
Example: ` job update myJobID pending
|
||||
job update myJobID pending --force true
|
||||
job update myJobID canceled --force true`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: withClient(updateJobCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
listJobsCmd.Flags().Int("page", 0, "Page number to fetch for the list of jobs")
|
||||
listJobsCmd.Flags().Int("per-page", 5, "Number of jobs to be fetched")
|
||||
listJobsCmd.Flags().Bool("all", false, "Fetch all jobs. --page flag will be ignored if provided")
|
||||
listJobsCmd.Flags().StringSlice("ids", nil, "Comma-separated list of job IDs to which the operation will be applied. All other flags are ignored")
|
||||
listJobsCmd.Flags().String("status", "", "Filter by job status")
|
||||
listJobsCmd.Flags().String("type", "", "Filter by job type")
|
||||
|
||||
createJobCmd.Flags().StringToString("data", nil, "Comma-separated list of key=value pairs passed to the job as type-specific options")
|
||||
|
||||
updateJobCmd.Flags().Bool("force", false, "Setting a job status is restricted to certain statuses. You can overwrite these restrictions by using --force. This might cause unexpected behaviour on your Mattermost Server. Use this option with caution.")
|
||||
|
||||
JobCmd.AddCommand(
|
||||
listJobsCmd,
|
||||
createJobCmd,
|
||||
showJobCmd,
|
||||
cancelJobCmd,
|
||||
updateJobCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(JobCmd)
|
||||
}
|
||||
|
||||
func listJobsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
ids, err := cmd.Flags().GetStringSlice("ids")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jobType, err := cmd.Flags().GetString("type")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, err := cmd.Flags().GetString("status")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
jobs := make([]*model.Job, 0, len(ids))
|
||||
var result *multierror.Error
|
||||
for _, id := range ids {
|
||||
isValidId := model.IsValidId(id)
|
||||
if !isValidId {
|
||||
result = multierror.Append(result, fmt.Errorf("invalid job ID: %s", id))
|
||||
continue
|
||||
}
|
||||
|
||||
job, _, err := c.GetJob(context.TODO(), id)
|
||||
if err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
continue
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
for _, job := range jobs {
|
||||
printJob(job)
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
return jobListCmdF(c, cmd, jobType, status)
|
||||
}
|
||||
|
||||
func updateJobCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
force, err := cmd.Flags().GetBool("force")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jobId := args[0]
|
||||
if !model.IsValidId(jobId) {
|
||||
return fmt.Errorf("invalid job ID: %s", jobId)
|
||||
}
|
||||
status := args[1]
|
||||
if !model.IsValidJobStatus(status) {
|
||||
return fmt.Errorf("invalid job status: %s", status)
|
||||
}
|
||||
|
||||
_, err = c.UpdateJobStatus(context.TODO(), jobId, status, force)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createJobCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
jobType := args[0]
|
||||
|
||||
data, err := cmd.Flags().GetStringToString("data")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job, _, err := c.CreateJob(context.TODO(), &model.Job{
|
||||
Type: jobType,
|
||||
Data: data,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create job: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Job successfully created, ID: {{.Id}}", job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func showJobCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
jobId := args[0]
|
||||
if !model.IsValidId(jobId) {
|
||||
return fmt.Errorf("invalid job ID: %s", jobId)
|
||||
}
|
||||
|
||||
job, _, err := c.GetJob(context.TODO(), jobId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get job: %w", err)
|
||||
}
|
||||
|
||||
printJob(job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelJobCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
jobId := args[0]
|
||||
if !model.IsValidId(jobId) {
|
||||
return fmt.Errorf("invalid job ID: %s", jobId)
|
||||
}
|
||||
|
||||
if _, err := c.CancelJob(context.TODO(), jobId); err != nil {
|
||||
return fmt.Errorf("failed to cancel job: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func jobTypeCompletionF(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return model.AllJobTypes[:], cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
func jobCompletionF(ctx context.Context, c client.Client, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return fetchAndComplete(
|
||||
func(ctx context.Context, c client.Client, page, perPage int) ([]*model.Job, *model.Response, error) {
|
||||
return c.GetJobs(ctx, "", "", page, perPage)
|
||||
},
|
||||
func(j *model.Job) []string { return []string{j.Id} },
|
||||
)(ctx, c, cmd, args, toComplete)
|
||||
}
|
||||
|
||||
func jobListCmdF(c client.Client, command *cobra.Command, jobType string, status string) error {
|
||||
page, err := command.Flags().GetInt("page")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
perPage, err := command.Flags().GetInt("per-page")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
showAll, err := command.Flags().GetBool("all")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if showAll {
|
||||
page = 0
|
||||
}
|
||||
|
||||
if jobType != "" && !model.IsValidJobType(jobType) {
|
||||
return fmt.Errorf("invalid job type: %s", jobType)
|
||||
}
|
||||
|
||||
if status != "" && !model.IsValidJobStatus(status) {
|
||||
return fmt.Errorf("invalid job status: %s", status)
|
||||
}
|
||||
|
||||
for {
|
||||
jobs, _, err := c.GetJobs(context.TODO(), jobType, status, page, perPage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get jobs: %w", err)
|
||||
}
|
||||
|
||||
if len(jobs) == 0 {
|
||||
if !showAll || page == 0 {
|
||||
printer.Print("No jobs found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
printJob(job)
|
||||
}
|
||||
|
||||
if !showAll {
|
||||
break
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printJob(job *model.Job) {
|
||||
if job.StartAt > 0 {
|
||||
printer.PrintT(fmt.Sprintf(` ID: {{.Id}}
|
||||
Type: {{.Type}}
|
||||
Status: {{.Status}}
|
||||
Created: %s
|
||||
Started: %s
|
||||
Data: {{.Data}}
|
||||
`,
|
||||
time.Unix(job.CreateAt/1000, 0), time.Unix(job.StartAt/1000, 0)), job)
|
||||
} else {
|
||||
printer.PrintT(fmt.Sprintf(` ID: {{.Id}}
|
||||
Status: {{.Status}}
|
||||
Created: %s
|
||||
`,
|
||||
time.Unix(job.CreateAt/1000, 0)), job)
|
||||
}
|
||||
}
|
||||
142
server/cmd/mmctl/commands/job_e2e_test.go
Normal file
142
server/cmd/mmctl/commands/job_e2e_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestCreateJobCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
newCmd := func() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringToString("data", nil, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
s.Run("create a job without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := createJobCmdF(s.th.Client, newCmd(), []string{model.JobTypeExportProcess})
|
||||
s.Require().EqualError(err, "failed to create job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("create a job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := createJobCmdF(c, newCmd(), []string{model.JobTypeExportProcess})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(model.JobTypeExportProcess, printer.GetLines()[0].(*model.Job).Type)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("create a job with data", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := newCmd()
|
||||
err := cmd.Flags().Set("data", "include_attachments=true")
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = createJobCmdF(c, cmd, []string{model.JobTypeExportProcess})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal("true", printer.GetLines()[0].(*model.Job).Data["include_attachments"])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestShowJobCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.Run("show a job without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := showJobCmdF(s.th.Client, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().EqualError(err, "failed to get job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("show a job that does not exist", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := showJobCmdF(c, &cobra.Command{}, []string{model.NewId()})
|
||||
s.Require().ErrorContains(err, "failed to get job: Unable to get the job.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("show a job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := showJobCmdF(c, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(job, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestCancelJobCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("cancel a job without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := cancelJobCmdF(s.th.Client, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().EqualError(err, "failed to cancel job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("cancel a job that does not exist", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := cancelJobCmdF(c, &cobra.Command{}, []string{model.NewId()})
|
||||
s.Require().ErrorContains(err, "failed to cancel job: Unable to get the job.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("cancel a job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeExportProcess,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
err := cancelJobCmdF(c, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
|
||||
// Refresh the job to confirm it was canceled.
|
||||
job, appErr = s.th.App.GetJob(s.th.Context, job.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal(model.JobStatusCanceled, job.Status)
|
||||
})
|
||||
}
|
||||
350
server/cmd/mmctl/commands/job_test.go
Normal file
350
server/cmd/mmctl/commands/job_test.go
Normal file
@@ -0,0 +1,350 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestListJobsCmdF() {
|
||||
s.Run("no jobs found", func() {
|
||||
printer.Clean()
|
||||
var mockJobs []*model.Job
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 10
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
cmd.Flags().StringSlice("ids", []string{}, "")
|
||||
cmd.Flags().String("status", "", "")
|
||||
cmd.Flags().String("type", "", "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "", "", 0, perPage).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := listJobsCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("3 jobs found", func() {
|
||||
printer.Clean()
|
||||
mockJobs := []*model.Job{
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 3
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
cmd.Flags().StringSlice("ids", []string{}, "")
|
||||
cmd.Flags().String("status", "", "")
|
||||
cmd.Flags().String("type", "", "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "", "", 0, perPage).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := listJobsCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), len(mockJobs))
|
||||
s.Empty(printer.GetErrorLines())
|
||||
for i, line := range printer.GetLines() {
|
||||
s.Equal(mockJobs[i], line.(*model.Job))
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("return 1 job using ids flag", func() {
|
||||
printer.Clean()
|
||||
id := model.NewId()
|
||||
mockJob := &model.Job{
|
||||
Id: id,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 3
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
cmd.Flags().StringSlice("ids", []string{id}, "")
|
||||
cmd.Flags().String("status", "", "")
|
||||
cmd.Flags().String("type", "", "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJob(context.TODO(), id).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := listJobsCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
for _, line := range printer.GetLines() {
|
||||
s.Equal(mockJob, line.(*model.Job))
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("return 2 jobs by status", func() {
|
||||
printer.Clean()
|
||||
mockJobs := []*model.Job{
|
||||
{
|
||||
Id: model.NewId(),
|
||||
Status: model.JobStatusSuccess,
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
Status: model.JobStatusSuccess,
|
||||
},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 2
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
cmd.Flags().String("status", model.JobStatusSuccess, "")
|
||||
cmd.Flags().StringSlice("ids", []string{}, "")
|
||||
cmd.Flags().String("type", "", "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), "", model.JobStatusSuccess, 0, perPage).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := listJobsCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), len(mockJobs))
|
||||
s.Empty(printer.GetErrorLines())
|
||||
for i, line := range printer.GetLines() {
|
||||
s.Equal(mockJobs[i], line.(*model.Job))
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("return 2 jobs by type", func() {
|
||||
printer.Clean()
|
||||
mockJobs := []*model.Job{
|
||||
{
|
||||
Id: model.NewId(),
|
||||
Type: model.JobTypeDataRetention,
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
Type: model.JobTypeDataRetention,
|
||||
},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 2
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
cmd.Flags().String("type", model.JobTypeDataRetention, "")
|
||||
cmd.Flags().StringSlice("ids", []string{}, "")
|
||||
cmd.Flags().String("status", "", "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), model.JobTypeDataRetention, "", 0, perPage).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := listJobsCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), len(mockJobs))
|
||||
s.Empty(printer.GetErrorLines())
|
||||
for i, line := range printer.GetLines() {
|
||||
s.Equal(mockJobs[i], line.(*model.Job))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestUpdateJobCmdF() {
|
||||
s.Run("update job status", func() {
|
||||
printer.Clean()
|
||||
id := model.NewId()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("force", true, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateJobStatus(context.TODO(), id, model.JobStatusPending, true).
|
||||
Return(&model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := updateJobCmdF(s.client, cmd, []string{id, model.JobStatusPending})
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestCreateJobCmdF() {
|
||||
s.Run("create job with data", func() {
|
||||
printer.Clean()
|
||||
data := map[string]string{"batch_size": "1000"}
|
||||
mockJob := &model.Job{
|
||||
Id: model.NewId(),
|
||||
Type: model.JobTypeMessageExport,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringToString("data", nil, "")
|
||||
err := cmd.Flags().Set("data", "batch_size=1000")
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateJob(context.TODO(), &model.Job{Type: model.JobTypeMessageExport, Data: data}).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = createJobCmdF(s.client, cmd, []string{model.JobTypeMessageExport})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
|
||||
s.Run("forwards type not in AllJobTypes to the server", func() {
|
||||
printer.Clean()
|
||||
mockJob := &model.Job{Id: model.NewId(), Type: model.JobTypeAccessControlSync}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringToString("data", nil, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateJob(context.TODO(), &model.Job{Type: model.JobTypeAccessControlSync, Data: map[string]string{}}).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := createJobCmdF(s.client, cmd, []string{model.JobTypeAccessControlSync})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.Run("create job returns server error", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringToString("data", nil, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateJob(context.TODO(), &model.Job{Type: model.JobTypeLdapSync, Data: map[string]string{}}).
|
||||
Return(nil, &model.Response{}, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
err := createJobCmdF(s.client, cmd, []string{model.JobTypeLdapSync})
|
||||
s.Require().NotNil(err)
|
||||
s.Empty(printer.GetLines())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestJobTypeCompletionF() {
|
||||
s.Run("suggests all job types for the first argument", func() {
|
||||
suggestions, directive := jobTypeCompletionF(&cobra.Command{}, []string{}, "")
|
||||
s.Equal(cobra.ShellCompDirectiveNoFileComp, directive)
|
||||
s.ElementsMatch(model.AllJobTypes[:], suggestions)
|
||||
})
|
||||
|
||||
s.Run("suggests nothing once the type is provided", func() {
|
||||
suggestions, directive := jobTypeCompletionF(&cobra.Command{}, []string{model.JobTypeLdapSync}, "")
|
||||
s.Equal(cobra.ShellCompDirectiveNoFileComp, directive)
|
||||
s.Empty(suggestions)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestShowJobCmdF() {
|
||||
s.Run("show job", func() {
|
||||
printer.Clean()
|
||||
id := model.NewId()
|
||||
mockJob := &model.Job{Id: id}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJob(context.TODO(), id).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := showJobCmdF(s.client, &cobra.Command{}, []string{id})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
|
||||
s.Run("show job with invalid ID", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := showJobCmdF(s.client, &cobra.Command{}, []string{"invalid-id"})
|
||||
s.Require().NotNil(err)
|
||||
s.Empty(printer.GetLines())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestCancelJobCmdF() {
|
||||
s.Run("cancel job", func() {
|
||||
printer.Clean()
|
||||
id := model.NewId()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CancelJob(context.TODO(), id).
|
||||
Return(&model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := cancelJobCmdF(s.client, &cobra.Command{}, []string{id})
|
||||
s.Require().Nil(err)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.Run("cancel job with invalid ID", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := cancelJobCmdF(s.client, &cobra.Command{}, []string{"invalid-id"})
|
||||
s.Require().NotNil(err)
|
||||
})
|
||||
|
||||
s.Run("cancel job returns server error", func() {
|
||||
printer.Clean()
|
||||
id := model.NewId()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CancelJob(context.TODO(), id).
|
||||
Return(&model.Response{}, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
err := cancelJobCmdF(s.client, &cobra.Command{}, []string{id})
|
||||
s.Require().NotNil(err)
|
||||
})
|
||||
}
|
||||
153
server/cmd/mmctl/commands/ldap.go
Normal file
153
server/cmd/mmctl/commands/ldap.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
var LdapCmd = &cobra.Command{
|
||||
Use: "ldap",
|
||||
Short: "LDAP related utilities",
|
||||
}
|
||||
|
||||
func newLDAPSyncCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Synchronize now",
|
||||
Long: "Synchronize all LDAP users and groups now.",
|
||||
Example: "mmctl ldap sync",
|
||||
RunE: withClient(ldapSyncCmdF),
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var LdapIDMigrate = &cobra.Command{
|
||||
Use: "idmigrate <objectGUID>",
|
||||
Short: "Migrate LDAP IdAttribute to new value",
|
||||
Long: `Migrate LDAP "IdAttribute" to a new value. Run this utility to change the value of your ID Attribute without your users losing their accounts. After running the command you can change the ID Attribute to the new value in the System Console. For example, if your current ID Attribute was "sAMAccountName" and you wanted to change it to "objectGUID", you would:
|
||||
|
||||
1. Wait for an off-peak time when your users won’t be impacted by a server restart.
|
||||
2. Run the command "mmctl ldap idmigrate objectGUID".
|
||||
3. Update the config within the System Console to the new value "objectGUID".
|
||||
4. Restart the Mattermost server.`,
|
||||
Example: "mmctl ldap idmigrate objectGUID",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(ldapIDMigrateCmdF),
|
||||
}
|
||||
|
||||
var LdapJobCmd = &cobra.Command{
|
||||
Use: "job",
|
||||
Short: "List and show LDAP sync jobs",
|
||||
}
|
||||
|
||||
var LdapJobListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Example: "mmctl ldap job list",
|
||||
Short: "List LDAP sync jobs",
|
||||
// Alisases cause error in zsh. Supposedly, completion V2 will fix that: https://github.com/spf13/cobra/pull/1146
|
||||
// https://mattermost.atlassian.net/browse/MM-57062
|
||||
// Aliases: []string{"ls"},
|
||||
Args: cobra.NoArgs,
|
||||
ValidArgsFunction: noCompletion,
|
||||
RunE: withClient(ldapJobListCmdF),
|
||||
}
|
||||
|
||||
var LdapJobShowCmd = &cobra.Command{
|
||||
Use: "show [ldapJobID]",
|
||||
Example: "mmctl ldap show f3d68qkkm7n8xgsfxwuo498rah",
|
||||
Short: "Show LDAP sync job",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
ValidArgsFunction: validateArgsWithClient(ldapJobShowCompletionF),
|
||||
RunE: withClient(ldapJobShowCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ldapSyncCmd := newLDAPSyncCmd()
|
||||
|
||||
LdapJobListCmd.Flags().Int("page", 0, "Page number to fetch for the list of import jobs")
|
||||
LdapJobListCmd.Flags().Int("per-page", 200, "Number of import jobs to be fetched")
|
||||
LdapJobListCmd.Flags().Bool("all", false, "Fetch all import jobs. --page flag will be ignore if provided")
|
||||
|
||||
LdapJobCmd.AddCommand(
|
||||
LdapJobListCmd,
|
||||
LdapJobShowCmd,
|
||||
)
|
||||
|
||||
LdapCmd.AddCommand(
|
||||
ldapSyncCmd,
|
||||
LdapIDMigrate,
|
||||
LdapJobCmd,
|
||||
)
|
||||
RootCmd.AddCommand(LdapCmd)
|
||||
}
|
||||
|
||||
func ldapSyncCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
resp, err := c.SyncLdap(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
printer.PrintT("Status: {{.status}}", map[string]any{"status": "ok"})
|
||||
} else {
|
||||
printer.PrintT("Status: {{.status}}", map[string]any{"status": "error"})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ldapIDMigrateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
toAttribute := args[0]
|
||||
resp, err := c.MigrateIdLdap(context.TODO(), toAttribute)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
printer.Print("AD/LDAP IdAttribute migration complete. You can now change your IdAttribute to: " + toAttribute)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ldapJobListCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
return jobListCmdF(c, command, model.JobTypeLdapSync, "")
|
||||
}
|
||||
|
||||
func ldapJobShowCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("expected at least one argument (ldapJobID). See help text for details")
|
||||
}
|
||||
|
||||
job, _, err := c.GetJob(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get LDAP sync job: %w", err)
|
||||
}
|
||||
|
||||
printJob(job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ldapJobShowCompletionF(ctx context.Context, c client.Client, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return fetchAndComplete(
|
||||
func(ctx context.Context, c client.Client, page, perPage int) ([]*model.Job, *model.Response, error) {
|
||||
return c.GetJobsByType(ctx, model.JobTypeLdapSync, page, perPage)
|
||||
},
|
||||
func(t *model.Job) []string { return []string{t.Id} },
|
||||
)(ctx, c, cmd, args, toComplete)
|
||||
}
|
||||
243
server/cmd/mmctl/commands/ldap_e2e_test.go
Normal file
243
server/cmd/mmctl/commands/ldap_e2e_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func configForLdap(th *api4.TestHelper) {
|
||||
ldapHost := os.Getenv("CI_LDAP_HOST")
|
||||
if ldapHost == "" {
|
||||
ldapHost = testutils.GetInterface(*th.App.Config().LdapSettings.LdapPort)
|
||||
}
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
|
||||
*cfg.LdapSettings.Enable = true
|
||||
*cfg.LdapSettings.EnableSync = true
|
||||
*cfg.LdapSettings.LdapServer = "dockerhost"
|
||||
*cfg.LdapSettings.BaseDN = "dc=mm,dc=test,dc=com"
|
||||
*cfg.LdapSettings.LdapServer = ldapHost
|
||||
*cfg.LdapSettings.BindUsername = "cn=admin,dc=mm,dc=test,dc=com"
|
||||
*cfg.LdapSettings.BindPassword = "mostest_password"
|
||||
*cfg.LdapSettings.FirstNameAttribute = "cn"
|
||||
*cfg.LdapSettings.LastNameAttribute = "sn"
|
||||
*cfg.LdapSettings.NicknameAttribute = "cn"
|
||||
*cfg.LdapSettings.EmailAttribute = "mail"
|
||||
*cfg.LdapSettings.UsernameAttribute = "uid"
|
||||
*cfg.LdapSettings.IdAttribute = "cn"
|
||||
*cfg.LdapSettings.LoginIdAttribute = "uid"
|
||||
*cfg.LdapSettings.SkipCertificateVerification = true
|
||||
*cfg.LdapSettings.GroupFilter = ""
|
||||
*cfg.LdapSettings.GroupDisplayNameAttribute = "cN"
|
||||
*cfg.LdapSettings.GroupIdAttribute = "entRyUuId"
|
||||
*cfg.LdapSettings.MaxPageSize = 0
|
||||
})
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestLdapSyncCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
configForLdap(s.th)
|
||||
|
||||
s.Run("MM-T3971 Should not allow regular user to start LDAP sync job", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := ldapSyncCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T2529 Should start LDAP sync job", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
jobs, appErr := s.th.App.GetJobsByTypePage(s.th.Context, model.JobTypeLdapSync, 0, 100)
|
||||
s.Require().Nil(appErr)
|
||||
initialNumJobs := len(jobs)
|
||||
|
||||
err := ldapSyncCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().NotEmpty(printer.GetLines())
|
||||
s.Require().Equal(printer.GetLines()[0], map[string]any{"status": "ok"})
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// we need to wait a bit for job creation
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, appErr = s.th.App.GetJobsByTypePage(s.th.Context, model.JobTypeLdapSync, 0, 100)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().NotEmpty(jobs)
|
||||
s.Assert().Equal(initialNumJobs+1, len(jobs))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestLdapIDMigrateCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
configForLdap(s.th)
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) { *cfg.LdapSettings.IdAttribute = "uid" })
|
||||
|
||||
// use existing ldap user from the test-data.ldif script
|
||||
// dn: uid=dev.one,ou=testusers,dc=mm,dc=test,dc=com
|
||||
// cn: Dev1
|
||||
// userPassword: Password1
|
||||
ldapUser, appErr := s.th.App.AuthenticateUserForLogin(s.th.Context, "", "dev.one", "Password1", "", "", true)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().NotNil(ldapUser)
|
||||
s.Require().Equal(model.UserAuthServiceLdap, ldapUser.AuthService)
|
||||
s.Require().Equal("dev.one", *ldapUser.AuthData)
|
||||
|
||||
s.Run("MM-T3973 Should not allow regular user to migrate LDAP ID attribute", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := ldapIDMigrateCmdF(s.th.Client, &cobra.Command{}, []string{"objectGUID"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3972 Should migrate LDAP ID attribute", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := ldapIDMigrateCmdF(c, &cobra.Command{}, []string{"cn"})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
s.Require().Nil(s.th.App.MigrateIdLDAP(s.th.Context, "uid"))
|
||||
}()
|
||||
|
||||
s.Require().NotEmpty(printer.GetLines())
|
||||
s.Require().Equal(printer.GetLines()[0], "AD/LDAP IdAttribute migration complete. You can now change your IdAttribute to: "+"cn")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
updatedUser, appErr := s.th.App.GetUser(s.th.Context, ldapUser.Id)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal("Dev1", *updatedUser.AuthData)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestLdapJobListCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
configForLdap(s.th)
|
||||
|
||||
s.Run("Should not allow regular user to list LDAP groups", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := ldapJobListCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("No LDAP jobs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
err := ldapJobListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("get some LDAP sync jobs", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 2
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
_, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeLdapSync,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job2, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeLdapSync,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
job3, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeLdapSync,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
err := ldapJobListCmdF(c, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), perPage)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Equal(job3, printer.GetLines()[0].(*model.Job))
|
||||
s.Require().Equal(job2, printer.GetLines()[1].(*model.Job))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestLdapJobShowCmdF() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
configForLdap(s.th)
|
||||
|
||||
job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{
|
||||
Type: model.JobTypeLdapSync,
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
s.Run("no permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := ldapJobShowCmdF(s.th.Client, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().EqualError(err, "failed to get LDAP sync job: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("no args", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := ldapJobShowCmdF(c, &cobra.Command{}, []string{})
|
||||
s.Require().EqualError(err, "expected at least one argument (ldapJobID). See help text for details")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("not found", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := ldapJobShowCmdF(c, &cobra.Command{}, []string{model.NewId()})
|
||||
s.Require().ErrorContains(err, "failed to get LDAP sync job: Unable to get the job.")
|
||||
s.Require().Empty(printer.GetLines())
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("found", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := ldapJobShowCmdF(c, &cobra.Command{}, []string{job.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(job, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
}
|
||||
255
server/cmd/mmctl/commands/ldap_test.go
Normal file
255
server/cmd/mmctl/commands/ldap_test.go
Normal file
@@ -0,0 +1,255 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestLdapSyncCmd() {
|
||||
s.Run("Sync without errors", func() {
|
||||
printer.Clean()
|
||||
outputMessage := map[string]any{"status": "ok"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
SyncLdap(context.TODO()).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := ldapSyncCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], outputMessage)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Not able to Sync", func() {
|
||||
printer.Clean()
|
||||
outputMessage := map[string]any{"status": "error"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
SyncLdap(context.TODO()).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, nil).
|
||||
Times(1)
|
||||
|
||||
err := ldapSyncCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], outputMessage)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Sync with response error", func() {
|
||||
printer.Clean()
|
||||
mockError := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
SyncLdap(context.TODO()).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockError).
|
||||
Times(1)
|
||||
|
||||
err := ldapSyncCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(err, mockError)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestLdapMigrateID() {
|
||||
s.Run("Run successfully without errors", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
MigrateIdLdap(context.TODO(), "test-id").
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := ldapIDMigrateCmdF(s.client, &cobra.Command{}, []string{"test-id"})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Contains(printer.GetLines()[0], "test-id")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Unable to migrate", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
MigrateIdLdap(context.TODO(), "test-id").
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, errors.New("test-error")).
|
||||
Times(1)
|
||||
|
||||
err := ldapIDMigrateCmdF(s.client, &cobra.Command{}, []string{"test-id"})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestLdapJobListCmdF() {
|
||||
s.Run("no LDAP jobs", func() {
|
||||
printer.Clean()
|
||||
var mockJobs []*model.Job
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 10
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), model.JobTypeLdapSync, "", 0, perPage).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := ldapJobListCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal("No jobs found", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("some LDAP jobs", func() {
|
||||
printer.Clean()
|
||||
mockJobs := []*model.Job{
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
{
|
||||
Id: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
perPage := 3
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", perPage, "")
|
||||
cmd.Flags().Bool("all", false, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobs(context.TODO(), model.JobTypeLdapSync, "", 0, perPage).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := ldapJobListCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), len(mockJobs))
|
||||
s.Empty(printer.GetErrorLines())
|
||||
for i, line := range printer.GetLines() {
|
||||
s.Equal(mockJobs[i], line.(*model.Job))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestLdapJobShowCmdF() {
|
||||
s.Run("not found", func() {
|
||||
printer.Clean()
|
||||
|
||||
jobID := model.NewId()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJob(context.TODO(), jobID).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusNotFound}, errors.New("not found")).
|
||||
Times(1)
|
||||
|
||||
err := ldapJobShowCmdF(s.client, &cobra.Command{}, []string{jobID})
|
||||
s.Require().NotNil(err)
|
||||
s.Empty(printer.GetLines())
|
||||
s.Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.Run("found", func() {
|
||||
printer.Clean()
|
||||
mockJob := &model.Job{
|
||||
Id: model.NewId(),
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJob(context.TODO(), mockJob.Id).
|
||||
Return(mockJob, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := ldapJobShowCmdF(s.client, &cobra.Command{}, []string{mockJob.Id})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Empty(printer.GetErrorLines())
|
||||
s.Equal(mockJob, printer.GetLines()[0].(*model.Job))
|
||||
})
|
||||
|
||||
s.Run("shell completion", func() {
|
||||
s.Run("no match for empty argument", func() {
|
||||
r, dir := ldapJobShowCompletionF(context.Background(), s.client, nil, nil, "")
|
||||
s.Equal(cobra.ShellCompDirectiveNoFileComp, dir)
|
||||
s.Equal([]string{}, r)
|
||||
})
|
||||
|
||||
s.Run("one element matches", func() {
|
||||
mockJobs := []*model.Job{
|
||||
{
|
||||
Id: "0_id",
|
||||
},
|
||||
{
|
||||
Id: "1_id",
|
||||
},
|
||||
{
|
||||
Id: "2_id",
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobsByType(context.Background(), model.JobTypeLdapSync, 0, DefaultPageSize).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
r, dir := ldapJobShowCompletionF(context.Background(), s.client, nil, nil, "1")
|
||||
s.Equal(cobra.ShellCompDirectiveNoFileComp, dir)
|
||||
s.Equal([]string{"1_id"}, r)
|
||||
})
|
||||
|
||||
s.Run("more elements then the limit match", func() {
|
||||
var mockJobs []*model.Job
|
||||
for i := range 100 {
|
||||
mockJobs = append(mockJobs, &model.Job{
|
||||
Id: fmt.Sprintf("id_%d", i),
|
||||
})
|
||||
}
|
||||
|
||||
var expected []string
|
||||
for i := range shellCompletionMaxItems {
|
||||
expected = append(expected, fmt.Sprintf("id_%d", i))
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetJobsByType(context.Background(), model.JobTypeLdapSync, 0, DefaultPageSize).
|
||||
Return(mockJobs, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
r, dir := ldapJobShowCompletionF(context.Background(), s.client, nil, nil, "id_")
|
||||
s.Equal(cobra.ShellCompDirectiveNoFileComp, dir)
|
||||
s.Equal(expected, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
145
server/cmd/mmctl/commands/license.go
Normal file
145
server/cmd/mmctl/commands/license.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var LicenseCmd = &cobra.Command{
|
||||
Use: "license",
|
||||
Short: "Licensing commands",
|
||||
}
|
||||
|
||||
var UploadLicenseCmd = &cobra.Command{
|
||||
Use: "upload [license]",
|
||||
Short: "Upload a license.",
|
||||
Long: "Upload a license. Replaces current license.",
|
||||
Example: " license upload /path/to/license/mylicensefile.mattermost-license",
|
||||
RunE: withClient(uploadLicenseCmdF),
|
||||
}
|
||||
|
||||
var UploadLicenseStringCmd = &cobra.Command{
|
||||
Use: "upload-string [license]",
|
||||
Short: "Upload a license from a string.",
|
||||
Long: "Upload a license from a string. Replaces current license.",
|
||||
Example: " license upload-string \"mylicensestring\"",
|
||||
RunE: withClient(uploadLicenseStringCmdF),
|
||||
}
|
||||
|
||||
var RemoveLicenseCmd = &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "Remove the current license.",
|
||||
Long: "Remove the current license and leave mattermost in Team Edition.",
|
||||
Example: " license remove",
|
||||
RunE: withClient(removeLicenseCmdF),
|
||||
}
|
||||
|
||||
var GetLicenseCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get the current license.",
|
||||
Long: "Get the current server license and print it.",
|
||||
Example: " license get",
|
||||
RunE: withClient(getLicenseCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
LicenseCmd.AddCommand(UploadLicenseCmd)
|
||||
LicenseCmd.AddCommand(RemoveLicenseCmd)
|
||||
LicenseCmd.AddCommand(UploadLicenseStringCmd)
|
||||
LicenseCmd.AddCommand(GetLicenseCmd)
|
||||
RootCmd.AddCommand(LicenseCmd)
|
||||
}
|
||||
|
||||
func uploadLicenseStringCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("enter one license file to upload")
|
||||
}
|
||||
|
||||
licenseBytes := []byte(args[0])
|
||||
|
||||
if _, err := c.UploadLicenseFile(context.TODO(), licenseBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Print("Uploaded license file")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadLicenseCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("enter one license file to upload")
|
||||
}
|
||||
|
||||
fileBytes, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.UploadLicenseFile(context.TODO(), fileBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Print("Uploaded license file")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeLicenseCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if _, err := c.RemoveLicenseFile(context.TODO()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Print("Removed license")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLicenseCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
license, _, err := c.GetOldClientLicense(context.TODO(), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if license["IsLicensed"] != "true" {
|
||||
printer.Print("No license installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format timestamps for human-readable output
|
||||
license["StartsAtReadable"] = formatLicenseTimestamp(license["StartsAt"])
|
||||
license["ExpiresAtReadable"] = formatLicenseTimestamp(license["ExpiresAt"])
|
||||
license["IssuedAtReadable"] = formatLicenseTimestamp(license["IssuedAt"])
|
||||
|
||||
printer.PrintT(`License ID: {{.Id}}
|
||||
Starts At: {{.StartsAtReadable}}
|
||||
Expires At: {{.ExpiresAtReadable}}
|
||||
Users: {{.Users}}
|
||||
SKU: {{.SkuShortName}}
|
||||
Is Trial: {{.IsTrial}}
|
||||
Issued At: {{.IssuedAtReadable}}
|
||||
Name: {{.Name}}
|
||||
Company: {{.Company}}`, license)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatLicenseTimestamp converts an epoch milliseconds string to a human-readable date.
|
||||
func formatLicenseTimestamp(epochMs string) string {
|
||||
ms, err := strconv.ParseInt(epochMs, 10, 64)
|
||||
if err != nil {
|
||||
return epochMs
|
||||
}
|
||||
return time.UnixMilli(ms).UTC().Format(time.RFC3339)
|
||||
}
|
||||
92
server/cmd/mmctl/commands/license_e2e_test.go
Normal file
92
server/cmd/mmctl/commands/license_e2e_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestRemoveLicenseCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Require().True(s.th.App.Srv().SetLicense(model.NewTestLicense()))
|
||||
|
||||
s.Run("MM-T3955 Should fail when regular user attempts to remove the server license", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := removeLicenseCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3954 Should be able to remove the server license", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := removeLicenseCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
s.Require().True(s.th.App.Srv().SetLicense(model.NewTestLicense()))
|
||||
}()
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Removed license")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
license := s.th.App.Srv().License()
|
||||
if model.BuildEnterpriseReady == "true" {
|
||||
// If an enterprise build is used,
|
||||
// the server resets to an Entry License.
|
||||
s.Require().NotNil(license)
|
||||
s.Require().True(license.IsMattermostEntry())
|
||||
} else {
|
||||
s.Require().Nil(license)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestUploadLicenseCmdF() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
// create temporary file
|
||||
tmpFile, err := os.CreateTemp(os.TempDir(), "testLicense-")
|
||||
s.Require().NoError(err)
|
||||
|
||||
license := model.NewTestLicense()
|
||||
b, err := json.Marshal(license)
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = tmpFile.Write(b)
|
||||
s.Require().NoError(err)
|
||||
s.T().Cleanup(func() {
|
||||
os.Remove(tmpFile.Name())
|
||||
})
|
||||
|
||||
s.Run("MM-T3953 Should fail when regular user attempts to upload a license file", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := uploadLicenseCmdF(s.th.Client, &cobra.Command{}, []string{tmpFile.Name()})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3952 Should be able to upload a license file, fail on validation", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := uploadLicenseCmdF(c, &cobra.Command{}, []string{tmpFile.Name()})
|
||||
s.Require().Error(err)
|
||||
appErr, ok := err.(*model.AppError)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(appErr.Message, "Invalid license file.")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
198
server/cmd/mmctl/commands/license_test.go
Normal file
198
server/cmd/mmctl/commands/license_test.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
const (
|
||||
fakeLicensePayload = "This is the license."
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestRemoveLicenseCmd() {
|
||||
s.Run("Remove license successfully", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveLicenseFile(context.TODO()).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, nil).
|
||||
Times(1)
|
||||
|
||||
err := removeLicenseCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(printer.GetLines()[0], "Removed license")
|
||||
})
|
||||
|
||||
s.Run("Fail to remove license", func() {
|
||||
printer.Clean()
|
||||
mockErr := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemoveLicenseFile(context.TODO()).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockErr).
|
||||
Times(1)
|
||||
|
||||
err := removeLicenseCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(err, mockErr)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestUploadLicenseCmdF() {
|
||||
// create temporary file
|
||||
tmpFile, err := os.CreateTemp(os.TempDir(), "testLicense-")
|
||||
s.NoError(err)
|
||||
|
||||
text := []byte(fakeLicensePayload)
|
||||
_, err = tmpFile.Write(text)
|
||||
s.NoError(err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
mockLicenseFile := []byte(fakeLicensePayload)
|
||||
|
||||
s.Run("Upload license successfully", func() {
|
||||
printer.Clean()
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadLicenseFile(context.TODO(), mockLicenseFile).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := uploadLicenseCmdF(s.client, &cobra.Command{}, []string{tmpFile.Name()})
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Fail to upload license if file not found", func() {
|
||||
printer.Clean()
|
||||
path := "/path/to/nonexistentfile"
|
||||
errMsg := "open " + path + ": no such file or directory"
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadLicenseFile(context.TODO(), mockLicenseFile).
|
||||
Times(0)
|
||||
|
||||
err := uploadLicenseCmdF(s.client, &cobra.Command{}, []string{path})
|
||||
s.Require().EqualError(err, errMsg)
|
||||
})
|
||||
|
||||
s.Run("Fail to upload license if no path is given", func() {
|
||||
printer.Clean()
|
||||
err := uploadLicenseCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().EqualError(err, "enter one license file to upload")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestUploadLicenseStringCmdF() {
|
||||
// create temporary file
|
||||
licenseString := string(fakeLicensePayload)
|
||||
|
||||
mockLicenseFile := []byte(fakeLicensePayload)
|
||||
|
||||
s.Run("Upload license successfully", func() {
|
||||
printer.Clean()
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadLicenseFile(context.TODO(), mockLicenseFile).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := uploadLicenseStringCmdF(s.client, &cobra.Command{}, []string{licenseString})
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Fail to upload license if no license string is given", func() {
|
||||
printer.Clean()
|
||||
err := uploadLicenseStringCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().EqualError(err, "enter one license file to upload")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestGetLicenseCmdF() {
|
||||
s.Run("Get license successfully", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockLicense := map[string]string{
|
||||
"IsLicensed": "true",
|
||||
"Id": "test-license-id",
|
||||
"Company": "Test Company",
|
||||
"Name": "Test Contact",
|
||||
"Email": "test@example.com",
|
||||
"SkuShortName": "enterprise",
|
||||
"Users": "100",
|
||||
"IssuedAt": "1609459200000",
|
||||
"StartsAt": "1609459200000",
|
||||
"ExpiresAt": "1640995200000",
|
||||
"IsTrial": "false",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetOldClientLicense(context.TODO(), "").
|
||||
Return(mockLicense, &model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := getLicenseCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// Verify human-readable dates are formatted correctly
|
||||
output := printer.GetLines()[0].(map[string]string)
|
||||
s.Equal("2021-01-01T00:00:00Z", output["IssuedAtReadable"])
|
||||
s.Equal("2021-01-01T00:00:00Z", output["StartsAtReadable"])
|
||||
s.Equal("2022-01-01T00:00:00Z", output["ExpiresAtReadable"])
|
||||
s.Equal("Test Company", output["Company"])
|
||||
s.Equal("enterprise", output["SkuShortName"])
|
||||
})
|
||||
|
||||
s.Run("No license installed", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockLicense := map[string]string{
|
||||
"IsLicensed": "false",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetOldClientLicense(context.TODO(), "").
|
||||
Return(mockLicense, &model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := getLicenseCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "No license installed")
|
||||
})
|
||||
|
||||
s.Run("Fail to get license", func() {
|
||||
printer.Clean()
|
||||
mockErr := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetOldClientLicense(context.TODO(), "").
|
||||
Return(nil, &model.Response{StatusCode: http.StatusInternalServerError}, mockErr).
|
||||
Times(1)
|
||||
|
||||
err := getLicenseCmdF(s.client, &cobra.Command{}, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(err, mockErr)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
}
|
||||
59
server/cmd/mmctl/commands/logs.go
Normal file
59
server/cmd/mmctl/commands/logs.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer/human"
|
||||
)
|
||||
|
||||
var LogsCmd = &cobra.Command{
|
||||
Use: "logs",
|
||||
Short: "Display logs in a human-readable format",
|
||||
Long: "Display logs in a human-readable format. As the logs format depends on the server, the \"--format\" flag cannot be used with this command.",
|
||||
RunE: withClient(logsCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
LogsCmd.Flags().IntP("number", "n", DefaultPageSize, "Number of log lines to retrieve.")
|
||||
LogsCmd.Flags().BoolP("logrus", "l", false, "Use logrus for formatting.")
|
||||
RootCmd.AddCommand(LogsCmd)
|
||||
}
|
||||
|
||||
func logsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("format") || cmd.Flags().Changed("json") {
|
||||
return fmt.Errorf("the %q and %q flags cannot be used with this command", "--format", "--json")
|
||||
} else if viper.GetString("format") == printer.FormatJSON {
|
||||
return fmt.Errorf("json formatting cannot be applied on this command. Please check the value of %q", "MMCTL_FORMAT")
|
||||
}
|
||||
|
||||
number, _ := cmd.Flags().GetInt("number")
|
||||
logLines, _, err := c.GetLogs(context.TODO(), 0, number)
|
||||
if err != nil {
|
||||
return errors.New("Unable to retrieve logs. Error: " + err.Error())
|
||||
}
|
||||
|
||||
reader := bytes.NewReader([]byte(strings.Join(logLines, "")))
|
||||
|
||||
var writer human.LogWriter
|
||||
if logrus, _ := cmd.Flags().GetBool("logrus"); logrus {
|
||||
writer = human.NewLogrusWriter(os.Stdout)
|
||||
} else {
|
||||
writer = human.NewSimpleWriter(os.Stdout)
|
||||
}
|
||||
human.ProcessLogs(reader, writer)
|
||||
|
||||
return nil
|
||||
}
|
||||
44
server/cmd/mmctl/commands/logs_e2e_test.go
Normal file
44
server/cmd/mmctl/commands/logs_e2e_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestlogsCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("Display single log line", func(c client.Client) {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("number", 1, "")
|
||||
|
||||
data, err := testLogsCmdF(c, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(data, 2)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Display in logrus for formatting", func(c client.Client) {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("logrus", true, "")
|
||||
cmd.Flags().Int("number", 1, "")
|
||||
|
||||
data, err := testLogsCmdF(c, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(data, 2)
|
||||
s.Contains(data[1], "time=")
|
||||
s.Contains(data[1], "level=")
|
||||
s.Contains(data[1], "msg=")
|
||||
})
|
||||
|
||||
s.Run("Should not allow normal user to retrieve logs", func() {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("number", 1, "")
|
||||
|
||||
_, err := testLogsCmdF(s.th.Client, cmd, []string{})
|
||||
s.Require().Error(err)
|
||||
})
|
||||
}
|
||||
149
server/cmd/mmctl/commands/logs_test.go
Normal file
149
server/cmd/mmctl/commands/logs_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
)
|
||||
|
||||
const (
|
||||
testLogInfo = `{"level":"info","ts":1573516747,"caller":"app/server.go:490","msg":"Server is listening on [::]:8065"}`
|
||||
testLogInfoStdout = "info app/server.go:490 Server is listening on [::]:8065"
|
||||
testLogrusStdout = "level=info msg=\"Server is listening on [::]:8065\" caller=\"app/server.go:490\""
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestLogsCmd() {
|
||||
s.Run("Display single log line", func() {
|
||||
mockSingleLogLine := []string{testLogInfo}
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("number", 1, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetLogs(context.TODO(), 0, 1).
|
||||
Return(mockSingleLogLine, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
data, err := testLogsCmdF(s.client, cmd, []string{})
|
||||
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(data, 1)
|
||||
s.Contains(data[0], testLogInfoStdout)
|
||||
})
|
||||
|
||||
s.Run("Display logs", func() {
|
||||
mockSingleLogLine := []string{testLogInfo}
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetLogs(context.TODO(), 0, 0).
|
||||
Return(mockSingleLogLine, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
data, err := testLogsCmdF(s.client, cmd, []string{})
|
||||
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(data, 1)
|
||||
s.Contains(data[0], testLogInfoStdout)
|
||||
})
|
||||
|
||||
s.Run("Display logs logrus format", func() {
|
||||
mockSingleLogLine := []string{testLogInfo}
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("logrus", true, "")
|
||||
cmd.Flags().Int("number", 1, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetLogs(context.TODO(), 0, 1).
|
||||
Return(mockSingleLogLine, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
data, err := testLogsCmdF(s.client, cmd, []string{})
|
||||
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(data, 1)
|
||||
s.Contains(data[0], testLogrusStdout)
|
||||
})
|
||||
|
||||
s.Run("Error when using format flag", func() {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("format", "json", "")
|
||||
cmd.Flags().Lookup("format").Changed = true
|
||||
|
||||
data, err := testLogsCmdF(s.client, cmd, []string{})
|
||||
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal(err.Error(), fmt.Sprintf("the %q and %q flags cannot be used with this command", "--format", "--json"))
|
||||
s.Require().Len(data, 0)
|
||||
|
||||
cmd.Flags().Lookup("format").Changed = false
|
||||
cmd.Flags().Bool("json", true, "")
|
||||
cmd.Flags().Lookup("json").Changed = true
|
||||
data, err = testLogsCmdF(s.client, cmd, []string{})
|
||||
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal(err.Error(), fmt.Sprintf("the %q and %q flags cannot be used with this command", "--format", "--json"))
|
||||
s.Require().Len(data, 0)
|
||||
})
|
||||
|
||||
s.Run("Error when setting json format with environment variable", func() {
|
||||
formatTmp := viper.GetString("format")
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
viper.Set("format", "json")
|
||||
|
||||
data, err := testLogsCmdF(s.client, cmd, []string{})
|
||||
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal(err.Error(), "json formatting cannot be applied on this command. Please check the value of \"MMCTL_FORMAT\"")
|
||||
s.Require().Len(data, 0)
|
||||
|
||||
viper.Set("format", formatTmp)
|
||||
})
|
||||
}
|
||||
|
||||
// testLogsCmdF is a wrapper around the logsCmdF function to capture
|
||||
// stdout for testing
|
||||
func testLogsCmdF(client client.Client, cmd *cobra.Command, args []string) ([]string, error) {
|
||||
// Redirect stdout
|
||||
currStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Call logsCmdF
|
||||
err := logsCmdF(client, cmd, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Stop capturing, set stdout back
|
||||
w.Close()
|
||||
os.Stdout = currStdout
|
||||
|
||||
// Copy to buffer
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Split for individual lines, removing last as it is an empty string
|
||||
data := strings.Split(buf.String(), "\n")
|
||||
data = data[:len(data)-1]
|
||||
|
||||
return data, err
|
||||
}
|
||||
26
server/cmd/mmctl/commands/main_test.go
Normal file
26
server/cmd/mmctl/commands/main_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build e2e
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var options = testlib.HelperOptions{
|
||||
EnableStore: true,
|
||||
EnableResources: true,
|
||||
}
|
||||
|
||||
mainHelper := testlib.NewMainHelperWithOptions(&options)
|
||||
api4.SetMainHelper(mainHelper)
|
||||
defer mainHelper.Close()
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
16
server/cmd/mmctl/commands/mmctl_e2e_test.go
Normal file
16
server/cmd/mmctl/commands/mmctl_e2e_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build e2e
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestMmctlE2ESuite(t *testing.T) {
|
||||
suite.Run(t, new(MmctlE2ETestSuite))
|
||||
}
|
||||
177
server/cmd/mmctl/commands/mmctl_test.go
Normal file
177
server/cmd/mmctl/commands/mmctl_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/mocks"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
var EnableEnterpriseTests string
|
||||
|
||||
type MmctlUnitTestSuite struct {
|
||||
suite.Suite
|
||||
mockCtrl *gomock.Controller
|
||||
client *mocks.MockClient
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) SetupTest() {
|
||||
viper.Reset()
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatJSON)
|
||||
|
||||
s.mockCtrl = gomock.NewController(s.T())
|
||||
s.client = mocks.NewMockClient(s.mockCtrl)
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TearDownTest() {
|
||||
s.mockCtrl.Finish()
|
||||
viper.Reset()
|
||||
}
|
||||
|
||||
type MmctlE2ETestSuite struct {
|
||||
suite.Suite
|
||||
th *api4.TestHelper
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) SetupTest() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatJSON)
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TearDownTest() {
|
||||
// Remove the test helper from the structure to avoid reusing the same helper between tests
|
||||
s.th = nil
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) SetupTestHelper() *api4.TestHelper {
|
||||
s.th = api4.Setup(s.T())
|
||||
return s.th
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) SetupTestHelperWithConfig(updateConfig func(cfg *model.Config)) *api4.TestHelper {
|
||||
s.th = api4.SetupConfig(s.T(), updateConfig)
|
||||
return s.th
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) SetupEnterpriseTestHelper() *api4.TestHelper {
|
||||
if EnableEnterpriseTests != "true" {
|
||||
s.T().SkipNow()
|
||||
}
|
||||
s.th = api4.SetupEnterprise(s.T())
|
||||
return s.th
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) SetupMessageExportTestHelper() *api4.TestHelper {
|
||||
if EnableEnterpriseTests != "true" {
|
||||
s.T().SkipNow()
|
||||
}
|
||||
|
||||
jobs.DefaultWatcherPollingInterval = 100
|
||||
s.th = api4.SetupEnterprise(s.T()).InitBasic(s.T())
|
||||
s.th.App.Srv().SetLicense(model.NewTestLicense("message_export"))
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.MessageExportSettings.DownloadExportResults = true
|
||||
*cfg.MessageExportSettings.EnableExport = true
|
||||
*cfg.MessageExportSettings.ExportFormat = model.ComplianceExportTypeActiance
|
||||
})
|
||||
|
||||
err := s.th.App.Srv().Jobs.StartWorkers()
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
err = s.th.App.Srv().Jobs.StartSchedulers()
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
return s.th
|
||||
}
|
||||
|
||||
// RunForSystemAdminAndLocal runs a test function for both SystemAdmin
|
||||
// and Local clients. Several commands work in the same way when used
|
||||
// by a fully privileged user and through the local mode, so this
|
||||
// helper facilitates checking both
|
||||
func (s *MmctlE2ETestSuite) RunForSystemAdminAndLocal(testName string, fn func(client.Client)) {
|
||||
s.Run(testName+"/SystemAdminClient", func() {
|
||||
fn(s.th.SystemAdminClient)
|
||||
})
|
||||
|
||||
s.Run(testName+"/LocalClient", func() {
|
||||
fn(s.th.LocalClient)
|
||||
})
|
||||
}
|
||||
|
||||
// RunForAllClients runs a test function for all the clients
|
||||
// registered in the TestHelper
|
||||
func (s *MmctlE2ETestSuite) RunForAllClients(testName string, fn func(client.Client)) {
|
||||
s.Run(testName+"/Client", func() {
|
||||
fn(s.th.Client)
|
||||
})
|
||||
|
||||
s.Run(testName+"/SystemAdminClient", func() {
|
||||
fn(s.th.SystemAdminClient)
|
||||
})
|
||||
|
||||
s.Run(testName+"/LocalClient", func() {
|
||||
fn(s.th.LocalClient)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) CheckErrorID(err error, errorId string) {
|
||||
api4.CheckErrorID(s.T(), err, errorId)
|
||||
}
|
||||
|
||||
// Helper functions for compliance export job testing
|
||||
|
||||
// getMostRecentJobWithId gets the most recent job with the specified ID
|
||||
func (s *MmctlE2ETestSuite) getMostRecentJobWithId(id string) *model.Job {
|
||||
list, _, err := s.th.SystemAdminClient.GetJobsByType(context.Background(), model.JobTypeMessageExport, 0, 1)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(list, 1)
|
||||
s.Require().Equal(id, list[0].Id)
|
||||
return list[0]
|
||||
}
|
||||
|
||||
// checkJobForStatus polls until the job with the specified ID reaches the expected status
|
||||
func (s *MmctlE2ETestSuite) checkJobForStatus(id string, status string) {
|
||||
doneChan := make(chan bool)
|
||||
var job *model.Job
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
for {
|
||||
job = s.getMostRecentJobWithId(id)
|
||||
if job.Status == status {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
s.Require().Equal(status, job.Status)
|
||||
}()
|
||||
select {
|
||||
case <-doneChan:
|
||||
case <-time.After(15 * time.Second):
|
||||
s.Require().Fail(fmt.Sprintf("expected job's status to be %s, got %s", status, job.Status))
|
||||
}
|
||||
}
|
||||
|
||||
// runJobForTest creates a job and waits for it to complete
|
||||
func (s *MmctlE2ETestSuite) runJobForTest(jobData map[string]string) *model.Job {
|
||||
job, _, err := s.th.SystemAdminClient.CreateJob(context.Background(),
|
||||
&model.Job{Type: model.JobTypeMessageExport, Data: jobData})
|
||||
s.Require().NoError(err)
|
||||
// poll until completion
|
||||
s.checkJobForStatus(job.Id, model.JobStatusSuccess)
|
||||
job = s.getMostRecentJobWithId(job.Id)
|
||||
return job
|
||||
}
|
||||
16
server/cmd/mmctl/commands/mmctl_unit_test.go
Normal file
16
server/cmd/mmctl/commands/mmctl_unit_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build unit
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestMmctlUnitSuite(t *testing.T) {
|
||||
suite.Run(t, new(MmctlUnitTestSuite))
|
||||
}
|
||||
83
server/cmd/mmctl/commands/oauth.go
Normal file
83
server/cmd/mmctl/commands/oauth.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var OAuthCmd = &cobra.Command{
|
||||
Use: "oauth",
|
||||
Short: "Management of OAuth2 apps",
|
||||
}
|
||||
|
||||
var ListOAuthAppsCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List OAuth2 apps",
|
||||
Long: "list all OAuth2 apps",
|
||||
Example: " oauth list",
|
||||
RunE: withClient(listOAuthAppsCmdF),
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func listOAuthAppsCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
page, err := command.Flags().GetInt("page")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
perPage, err := command.Flags().GetInt("per-page")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apps, _, err := c.GetOAuthApps(context.Background(), page, perPage)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to fetch oauth2 apps")
|
||||
}
|
||||
|
||||
userIds := make([]string, len(apps))
|
||||
for i := range apps {
|
||||
userIds[i] = apps[i].CreatorId
|
||||
}
|
||||
|
||||
users, _, err := c.GetUsersByIds(context.Background(), userIds)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to fetch users for oauth2 apps")
|
||||
}
|
||||
|
||||
usersByID := map[string]*model.User{}
|
||||
for _, user := range users {
|
||||
usersByID[user.Id] = user
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
ownerName := app.CreatorId
|
||||
if owner, ok := usersByID[app.CreatorId]; ok {
|
||||
ownerName = owner.Username
|
||||
}
|
||||
printer.PrintT(fmt.Sprintf("{{.Id}}: {{.Name}} (Created by %s)", ownerName), app)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ListOAuthAppsCmd.Flags().Int("page", 0, "Page number to fetch for the list of OAuth2 apps")
|
||||
ListOAuthAppsCmd.Flags().Int("per-page", DefaultPageSize, "Number of OAuth2 apps to be fetched")
|
||||
|
||||
OAuthCmd.AddCommand(
|
||||
ListOAuthAppsCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(OAuthCmd)
|
||||
}
|
||||
138
server/cmd/mmctl/commands/oauth_test.go
Normal file
138
server/cmd/mmctl/commands/oauth_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestListOAuthAppsCmd() {
|
||||
oauthAppID := "oauthAppID"
|
||||
oauthAppName := "oauthAppName"
|
||||
userID := "userID"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
|
||||
s.Run("Listing oauth apps", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockOAuthApp := model.OAuthApp{
|
||||
Id: oauthAppID,
|
||||
Name: oauthAppName,
|
||||
CreatorId: userID,
|
||||
}
|
||||
mockUser := model.User{Id: mockOAuthApp.CreatorId, Username: "mockuser"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetOAuthApps(context.Background(), 0, 200).
|
||||
Return([]*model.OAuthApp{&mockOAuthApp}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.Background(), []string{mockOAuthApp.CreatorId}).
|
||||
Return([]*model.User{&mockUser}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := listOAuthAppsCmdF(s.client, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(&mockOAuthApp, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Listing oauth apps with paging", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockOAuthApp := model.OAuthApp{
|
||||
Id: oauthAppID,
|
||||
Name: oauthAppName,
|
||||
CreatorId: userID,
|
||||
}
|
||||
mockUser := model.User{Id: mockOAuthApp.CreatorId, Username: "mockuser"}
|
||||
|
||||
pageCmd := &cobra.Command{}
|
||||
pageCmd.Flags().Int("page", 0, "")
|
||||
pageCmd.Flags().Int("per-page", 200, "")
|
||||
|
||||
page := 1
|
||||
perPage := 2
|
||||
_ = pageCmd.Flags().Set("page", strconv.Itoa(page))
|
||||
_ = pageCmd.Flags().Set("per-page", strconv.Itoa(perPage))
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetOAuthApps(context.Background(), 1, 2).
|
||||
Return([]*model.OAuthApp{&mockOAuthApp}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.Background(), []string{mockOAuthApp.CreatorId}).
|
||||
Return([]*model.User{&mockUser}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := listOAuthAppsCmdF(s.client, pageCmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(&mockOAuthApp, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Unable to list oauth apps", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockError := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetOAuthApps(context.Background(), 0, 200).
|
||||
Return(nil, &model.Response{}, mockError).
|
||||
Times(1)
|
||||
|
||||
err := listOAuthAppsCmdF(s.client, cmd, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.EqualError(err, "Failed to fetch oauth2 apps: mock error")
|
||||
})
|
||||
|
||||
s.Run("Unable to get users for oauth apps", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockOAuthApp := model.OAuthApp{
|
||||
Id: oauthAppID,
|
||||
Name: oauthAppName,
|
||||
CreatorId: userID,
|
||||
}
|
||||
mockError := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetOAuthApps(context.Background(), 0, 200).
|
||||
Return([]*model.OAuthApp{&mockOAuthApp}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUsersByIds(context.Background(), []string{mockOAuthApp.CreatorId}).
|
||||
Return(nil, &model.Response{}, mockError).
|
||||
Times(1)
|
||||
|
||||
err := listOAuthAppsCmdF(s.client, cmd, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.EqualError(err, "Failed to fetch users for oauth2 apps: mock error")
|
||||
})
|
||||
}
|
||||
408
server/cmd/mmctl/commands/permission_role_test.go
Normal file
408
server/cmd/mmctl/commands/permission_role_test.go
Normal file
@@ -0,0 +1,408 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestAssignUsersCmd() {
|
||||
s.Run("Assigning a user to a role", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit"},
|
||||
}
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user1",
|
||||
Roles: "system_user",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), mockUser.Username, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateUserRoles(context.TODO(), mockUser.Id, fmt.Sprintf("%s %s", mockUser.Roles, mockRole.Name)).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name, mockUser.Username}
|
||||
err := assignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Assigning multiple users to a role", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit"},
|
||||
}
|
||||
|
||||
mockUser1 := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user1",
|
||||
Roles: "system_user",
|
||||
}
|
||||
|
||||
mockUser2 := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user2",
|
||||
Roles: "system_user system_admin",
|
||||
}
|
||||
|
||||
notFoundUser := &model.User{
|
||||
Username: "notfound",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
for _, user := range []*model.User{mockUser1, mockUser2} {
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), user.Username, "").
|
||||
Return(user, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateUserRoles(context.TODO(), user.Id, fmt.Sprintf("%s %s", user.Roles, mockRole.Name)).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), notFoundUser.Username, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), notFoundUser.Username, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
expectedError := &multierror.Error{}
|
||||
expectedError = multierror.Append(expectedError, fmt.Errorf("couldn't find user 'notfound'"))
|
||||
|
||||
args := []string{mockRole.Name, mockUser1.Username, notFoundUser.Username, mockUser2.Username}
|
||||
err := assignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(expectedError.ErrorOrNil(), err)
|
||||
})
|
||||
|
||||
s.Run("Assigning to a non-existent role", func() {
|
||||
expectedError := errors.New("role_not_found")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), "non-existent").
|
||||
Return(nil, &model.Response{StatusCode: http.StatusNotFound}, expectedError).
|
||||
Times(1)
|
||||
|
||||
args := []string{"non-existent", "user1"}
|
||||
err := assignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(expectedError, err)
|
||||
})
|
||||
|
||||
s.Run("Assigning a user to a role that is already assigned", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit"},
|
||||
}
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user1",
|
||||
Roles: "system_user mock-role",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), mockUser.Username, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name, mockUser.Username}
|
||||
err := assignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Assigning a user that is not found", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit"},
|
||||
}
|
||||
|
||||
requestedUser := "user99"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), requestedUser, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), requestedUser, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
expectedError := &multierror.Error{}
|
||||
expectedError = multierror.Append(expectedError, fmt.Errorf("couldn't find user '%s'", requestedUser))
|
||||
|
||||
args := []string{mockRole.Name, requestedUser}
|
||||
err := assignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(expectedError.ErrorOrNil(), err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestUnassignUsersCmd() {
|
||||
s.Run("Unassigning a user from a role", func() {
|
||||
roleName := "mock-role"
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user1",
|
||||
Roles: fmt.Sprintf("system_user %s team_admin", roleName),
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), mockUser.Username, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateUserRoles(context.TODO(), mockUser.Id, "system_user team_admin").
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{roleName, mockUser.Username}
|
||||
err := unassignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Unassign multiple users from a role", func() {
|
||||
roleName := "mock-role"
|
||||
|
||||
mockUser1 := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user1",
|
||||
Roles: "system_user mock-role",
|
||||
}
|
||||
|
||||
mockUser2 := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user2",
|
||||
Roles: "system_user system_admin mock-role",
|
||||
}
|
||||
|
||||
notFoundUser := &model.User{
|
||||
Username: "notfound",
|
||||
}
|
||||
|
||||
for _, user := range []*model.User{mockUser1, mockUser2} {
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), user.Username, "").
|
||||
Return(user, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateUserRoles(context.TODO(), user.Id, strings.TrimSpace(strings.ReplaceAll(user.Roles, roleName, ""))).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), notFoundUser.Username, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), notFoundUser.Username, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{roleName, mockUser1.Username, notFoundUser.Username, mockUser2.Username}
|
||||
err := unassignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Unassign from a non-assigned or role", func() {
|
||||
roleName := "mock-role"
|
||||
|
||||
mockUser := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user1",
|
||||
Roles: "system_user",
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), mockUser.Username, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{roleName, mockUser.Username}
|
||||
err := unassignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Unassigning a user that is not found", func() {
|
||||
requestedUser := "user99"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), requestedUser, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), requestedUser, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{"mock-role-id", requestedUser}
|
||||
err := unassignUsersCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestShowRoleCmd() {
|
||||
s.Run("Show custom role", func() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
defer printer.SetFormat(printer.FormatJSON)
|
||||
|
||||
commandArg := "example-role-name"
|
||||
mockRole := &model.Role{
|
||||
Id: "example-mock-id",
|
||||
Name: commandArg,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := showRoleCmdF(s.client, &cobra.Command{}, []string{commandArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Equal(`
|
||||
Property Value
|
||||
-------- -----
|
||||
Name example-role-name
|
||||
DisplayName
|
||||
BuiltIn false
|
||||
SchemeManaged false
|
||||
`, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Show a role with a sysconsole_* permission", func() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
defer printer.SetFormat(printer.FormatJSON)
|
||||
|
||||
commandArg := "example-role-name"
|
||||
mockRole := &model.Role{
|
||||
Id: "example-mock-id",
|
||||
Name: commandArg,
|
||||
Permissions: []string{"sysconsole_write_site", "edit_brand"},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := showRoleCmdF(s.client, &cobra.Command{}, []string{commandArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Equal(`
|
||||
Property Value Used by
|
||||
-------- ----- -------
|
||||
Name example-role-name
|
||||
DisplayName
|
||||
BuiltIn false
|
||||
SchemeManaged false
|
||||
Permissions edit_brand
|
||||
sysconsole_write_site
|
||||
`, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Show custom role with invalid name", func() {
|
||||
printer.Clean()
|
||||
|
||||
expectedError := errors.New("role_not_found")
|
||||
|
||||
commandArgBogus := "bogus-role-name"
|
||||
|
||||
// showRoleCmdF will look up role by name
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), commandArgBogus).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusNotFound}, expectedError).
|
||||
Times(1)
|
||||
|
||||
err := showRoleCmdF(s.client, &cobra.Command{}, []string{commandArgBogus})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(expectedError, err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
173
server/cmd/mmctl/commands/permissions.go
Normal file
173
server/cmd/mmctl/commands/permissions.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var PermissionsCmd = &cobra.Command{
|
||||
Use: "permissions",
|
||||
Short: "Management of permissions",
|
||||
}
|
||||
|
||||
var AddPermissionsCmd = &cobra.Command{
|
||||
Use: "add <role> <permission...>",
|
||||
Short: "Add permissions to a role (EE Only)",
|
||||
Long: `Add one or more permissions to an existing role (Only works in Enterprise Edition).`,
|
||||
Example: ` permissions add system_user list_open_teams
|
||||
permissions add system_manager sysconsole_read_user_management_channels`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: withClient(addPermissionsCmdF),
|
||||
}
|
||||
|
||||
var RemovePermissionsCmd = &cobra.Command{
|
||||
Use: "remove <role> <permission...>",
|
||||
Short: "Remove permissions from a role (EE Only)",
|
||||
Long: `Remove one or more permissions from an existing role (Only works in Enterprise Edition).`,
|
||||
Example: ` permissions remove system_user list_open_teams
|
||||
permissions remove system_manager sysconsole_read_user_management_channels`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: withClient(removePermissionsCmdF),
|
||||
}
|
||||
|
||||
var ResetCmd = &cobra.Command{
|
||||
Use: "reset <role_name>",
|
||||
Short: "Reset default permissions for role (EE Only)",
|
||||
Long: "Reset the given role's permissions to the set that was originally released with",
|
||||
Example: ` # Reset the permissions of the 'system_read_only_admin' role.
|
||||
$ mmctl permissions reset system_read_only_admin`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(resetPermissionsCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
PermissionsCmd.AddCommand(
|
||||
AddPermissionsCmd,
|
||||
RemovePermissionsCmd,
|
||||
ResetCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(PermissionsCmd)
|
||||
}
|
||||
|
||||
func addPermissionsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
role, _, err := c.GetRoleByName(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newPermissions := role.Permissions
|
||||
|
||||
for _, permissionID := range args[1:] {
|
||||
newPermissions = append(newPermissions, permissionID)
|
||||
|
||||
if ancillaryPermissions, ok := model.SysconsoleAncillaryPermissions[permissionID]; ok {
|
||||
for _, ancillaryPermission := range ancillaryPermissions {
|
||||
newPermissions = append(newPermissions, ancillaryPermission.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patchRole := model.RolePatch{
|
||||
Permissions: &newPermissions,
|
||||
}
|
||||
|
||||
if _, _, err = c.PatchRole(context.TODO(), role.Id, &patchRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removePermissionsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
role, _, err := c.GetRoleByName(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newPermissionSet := role.Permissions
|
||||
for _, permissionID := range args[1:] {
|
||||
newPermissionSet = removeFromStringSlice(newPermissionSet, permissionID)
|
||||
}
|
||||
|
||||
var ancillaryPermissionsStillUsed []*model.Permission
|
||||
for _, permissionID := range newPermissionSet {
|
||||
if ancillaryPermissions, ok := model.SysconsoleAncillaryPermissions[permissionID]; ok {
|
||||
ancillaryPermissionsStillUsed = append(ancillaryPermissionsStillUsed, ancillaryPermissions...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, permissionID := range args[1:] {
|
||||
if ancillaryPermissions, ok := model.SysconsoleAncillaryPermissions[permissionID]; ok {
|
||||
for _, permission := range ancillaryPermissions {
|
||||
if !permissionsSliceIncludes(ancillaryPermissionsStillUsed, permission) {
|
||||
newPermissionSet = removeFromStringSlice(newPermissionSet, permission.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patchRole := model.RolePatch{
|
||||
Permissions: &newPermissionSet,
|
||||
}
|
||||
|
||||
if _, _, err = c.PatchRole(context.TODO(), role.Id, &patchRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetPermissionsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
role, _, err := c.GetRoleByName(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultRole, ok := model.MakeDefaultRoles()[role.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("no default permissions available for role")
|
||||
}
|
||||
|
||||
patchRole := model.RolePatch{
|
||||
Permissions: &defaultRole.Permissions,
|
||||
}
|
||||
|
||||
role, _, err = c.PatchRole(context.TODO(), role.Id, &patchRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.PrintT(prettyRole(role), nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeFromStringSlice(items []string, item string) []string {
|
||||
newPermissions := []string{}
|
||||
for _, x := range items {
|
||||
if x != item {
|
||||
newPermissions = append(newPermissions, x)
|
||||
}
|
||||
}
|
||||
return newPermissions
|
||||
}
|
||||
|
||||
func permissionsSliceIncludes(haystack []*model.Permission, needle *model.Permission) bool {
|
||||
for _, item := range haystack {
|
||||
if item.Id == needle.Id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
108
server/cmd/mmctl/commands/permissions_e2e_test.go
Normal file
108
server/cmd/mmctl/commands/permissions_e2e_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestShowRoleCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForAllClients("MM-T3928 Should allow all users to see a role", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := showRoleCmdF(c, &cobra.Command{}, []string{model.SystemAdminRoleId})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForAllClients("MM-T3959 Should return error to all users for a none exitent role", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := showRoleCmdF(c, &cobra.Command{}, []string{"none_existent_role"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestAddPermissionsCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
role, appErr := s.th.App.GetRoleByName(s.th.Context, model.SystemUserRoleId)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().NotContains(role.Permissions, model.PermissionCreateBot.Id)
|
||||
|
||||
s.Run("MM-T3961 Should not allow normal user to add a permission to a role", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := addPermissionsCmdF(s.th.Client, &cobra.Command{}, []string{model.SystemUserRoleId, model.PermissionCreateBot.Id})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3960 Should be able to add a permission to a role", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := addPermissionsCmdF(c, &cobra.Command{}, []string{model.SystemUserRoleId, model.PermissionCreateBot.Id})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
permissions := role.Permissions
|
||||
newRole, appErr := s.th.App.PatchRole(role, &model.RolePatch{Permissions: &permissions})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().NotContains(newRole.Permissions, model.PermissionCreateBot.Id)
|
||||
}()
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
updatedRole, appErr := s.th.App.GetRoleByName(s.th.Context, model.SystemUserRoleId)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Contains(updatedRole.Permissions, model.PermissionCreateBot.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestRemovePermissionsCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
role, appErr := s.th.App.GetRoleByName(s.th.Context, model.SystemUserRoleId)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Contains(role.Permissions, model.PermissionCreateDirectChannel.Id)
|
||||
|
||||
s.Run("MM-T3963 Should not allow normal user to remove a permission from a role", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := removePermissionsCmdF(s.th.Client, &cobra.Command{}, []string{model.SystemUserRoleId, model.PermissionCreateDirectChannel.Id})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3962 Should be able to remove a permission from a role", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := removePermissionsCmdF(c, &cobra.Command{}, []string{model.SystemUserRoleId, model.PermissionCreateDirectChannel.Id})
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
permissions := []string{model.PermissionCreateDirectChannel.Id}
|
||||
newRole, appErr := s.th.App.PatchRole(role, &model.RolePatch{Permissions: &permissions})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Contains(newRole.Permissions, model.PermissionCreateDirectChannel.Id)
|
||||
}()
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
updatedRole, appErr := s.th.App.GetRoleByName(s.th.Context, model.SystemUserRoleId)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().NotContains(updatedRole.Permissions, model.PermissionCreateDirectChannel.Id)
|
||||
})
|
||||
}
|
||||
80
server/cmd/mmctl/commands/permissions_reset_e2e_test.go
Normal file
80
server/cmd/mmctl/commands/permissions_reset_e2e_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestResetPermissionsCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("Shouldn't let a non-system-admin reset a role's permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
// update the role to have some non-default permissions
|
||||
role, err := s.th.App.GetRoleByName(s.th.Context, model.SystemUserManagerRoleId)
|
||||
s.Require().Nil(err)
|
||||
|
||||
defaultPermissions := role.Permissions
|
||||
expectedPermissions := []string{model.PermissionUseGroupMentions.Id, model.PermissionUseChannelMentions.Id}
|
||||
role.Permissions = expectedPermissions
|
||||
role, err = s.th.App.UpdateRole(role)
|
||||
s.Require().Nil(err)
|
||||
|
||||
// reset to defaults when we're done
|
||||
defer func() {
|
||||
role.Permissions = defaultPermissions
|
||||
_, err = s.th.App.UpdateRole(role)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
// try to reset the permissions
|
||||
err2 := resetPermissionsCmdF(s.th.Client, &cobra.Command{}, []string{model.SystemUserManagerRoleId})
|
||||
s.Require().Error(err2)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// ensure reset didn't happen
|
||||
roleAfterResetAttempt, err := s.th.App.GetRoleByName(s.th.Context, model.SystemUserManagerRoleId)
|
||||
s.Require().Nil(err)
|
||||
s.Require().ElementsMatch(expectedPermissions, roleAfterResetAttempt.Permissions)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Reset a role's permissions", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
// update the role to have some non-default permissions
|
||||
role, err := s.th.App.GetRoleByName(s.th.Context, model.SystemUserManagerRoleId)
|
||||
s.Require().Nil(err)
|
||||
|
||||
defaultPermissions := role.Permissions
|
||||
expectedPermissions := []string{model.PermissionUseGroupMentions.Id, model.PermissionUseChannelMentions.Id}
|
||||
role.Permissions = expectedPermissions
|
||||
role, err = s.th.App.UpdateRole(role)
|
||||
|
||||
// reset to defaults when we're done
|
||||
defer func() {
|
||||
role.Permissions = defaultPermissions
|
||||
_, err = s.th.App.UpdateRole(role)
|
||||
s.Require().Nil(err)
|
||||
}()
|
||||
|
||||
// try to reset the permissions
|
||||
err2 := resetPermissionsCmdF(c, &cobra.Command{}, []string{model.SystemUserManagerRoleId})
|
||||
s.Require().Nil(err2)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// ensure reset was successful
|
||||
roleAfterResetAttempt, err := s.th.App.GetRoleByName(s.th.Context, model.SystemUserManagerRoleId)
|
||||
s.Require().Nil(err)
|
||||
s.Require().ElementsMatch(defaultPermissions, roleAfterResetAttempt.Permissions)
|
||||
})
|
||||
}
|
||||
227
server/cmd/mmctl/commands/permissions_role.go
Normal file
227
server/cmd/mmctl/commands/permissions_role.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var RoleCmd = &cobra.Command{
|
||||
Use: "role",
|
||||
Short: "Management of roles",
|
||||
}
|
||||
|
||||
var ShowCmd = &cobra.Command{
|
||||
Use: "show <role_name>",
|
||||
Short: "Show the role information",
|
||||
Long: "Show all the information about a role.",
|
||||
Example: ` permissions role show system_user`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(showRoleCmdF),
|
||||
}
|
||||
|
||||
var AssignCmd = &cobra.Command{
|
||||
Use: "assign <role_name> <username...>",
|
||||
Short: "Assign users to role (EE Only)",
|
||||
Long: "Assign users to a role by username (Only works in Enterprise Edition).",
|
||||
Example: ` # Assign users with usernames 'john.doe' and 'jane.doe' to the role named 'system_admin'.
|
||||
permissions assign system_admin john.doe jane.doe
|
||||
|
||||
# Examples using other system roles
|
||||
permissions assign system_manager john.doe jane.doe
|
||||
permissions assign system_user_manager john.doe jane.doe
|
||||
permissions assign system_read_only_admin john.doe jane.doe`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: withClient(assignUsersCmdF),
|
||||
}
|
||||
|
||||
var UnassignCmd = &cobra.Command{
|
||||
Use: "unassign <role_name> <username...>",
|
||||
Short: "Unassign users from role (EE Only)",
|
||||
Long: "Unassign users from a role by username (Only works in Enterprise Edition).",
|
||||
Example: ` # Unassign users with usernames 'john.doe' and 'jane.doe' from the role named 'system_admin'.
|
||||
permissions unassign system_admin john.doe jane.doe
|
||||
|
||||
# Examples using other system roles
|
||||
permissions unassign system_manager john.doe jane.doe
|
||||
permissions unassign system_user_manager john.doe jane.doe
|
||||
permissions unassign system_read_only_admin john.doe jane.doe`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: withClient(unassignUsersCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
RoleCmd.AddCommand(
|
||||
AssignCmd,
|
||||
UnassignCmd,
|
||||
ShowCmd,
|
||||
)
|
||||
|
||||
PermissionsCmd.AddCommand(
|
||||
RoleCmd,
|
||||
)
|
||||
}
|
||||
|
||||
func prettyRole(role *model.Role) string {
|
||||
slices.Sort(role.Permissions)
|
||||
|
||||
consolePermissionMap := map[string]bool{}
|
||||
for _, perm := range role.Permissions {
|
||||
if strings.HasPrefix(perm, "sysconsole_") {
|
||||
consolePermissionMap[perm] = true
|
||||
}
|
||||
}
|
||||
|
||||
getUsedBy := func(permissionID string) []string {
|
||||
var usedByIDs []string
|
||||
if !strings.HasPrefix(permissionID, "sysconsole_") {
|
||||
usedBy := map[string]bool{} // map to make a unique set
|
||||
for key, vals := range model.SysconsoleAncillaryPermissions {
|
||||
for _, val := range vals {
|
||||
if val.Id == permissionID {
|
||||
if _, ok := consolePermissionMap[key]; ok {
|
||||
usedBy[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for key := range usedBy {
|
||||
usedByIDs = append(usedByIDs, key)
|
||||
}
|
||||
}
|
||||
return usedByIDs
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
w := tabwriter.NewWriter(&b, 0, 0, 1, ' ', 0)
|
||||
|
||||
// Only show the 3-column view if the role has sysconsole permissions
|
||||
// sysadmin has every permission, so no point in showing the "Used by"
|
||||
// column.
|
||||
if len(consolePermissionMap) > 0 && role.Name != "system_admin" {
|
||||
fmt.Fprintf(w, "\nProperty\tValue\tUsed by\n")
|
||||
fmt.Fprintf(w, "--------\t-----\t-------\n")
|
||||
fmt.Fprintf(w, "Name\t%s\t\n", role.Name)
|
||||
fmt.Fprintf(w, "DisplayName\t%s\t\n", role.DisplayName)
|
||||
fmt.Fprintf(w, "BuiltIn\t%v\t\n", role.BuiltIn)
|
||||
fmt.Fprintf(w, "SchemeManaged\t%v\t\n", role.SchemeManaged)
|
||||
for i, perm := range role.Permissions {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "Permissions\t%s\t%v\n", role.Permissions[0], strings.Join(getUsedBy(role.Permissions[0]), ", "))
|
||||
} else {
|
||||
fmt.Fprintf(w, "\t%s\t%v\n", perm, strings.Join(getUsedBy(perm), ", "))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, "\nProperty\tValue\n")
|
||||
fmt.Fprintf(w, "--------\t-----\n")
|
||||
fmt.Fprintf(w, "Name\t%s\n", role.Name)
|
||||
fmt.Fprintf(w, "DisplayName\t%s\n", role.DisplayName)
|
||||
fmt.Fprintf(w, "BuiltIn\t%v\n", role.BuiltIn)
|
||||
fmt.Fprintf(w, "SchemeManaged\t%v\n", role.SchemeManaged)
|
||||
for i, perm := range role.Permissions {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "Permissions\t%s\n", role.Permissions[0])
|
||||
} else {
|
||||
fmt.Fprintf(w, "\t%s\n", perm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func showRoleCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
role, _, err := c.GetRoleByName(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.PrintT(prettyRole(role), nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func assignUsersCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
role, _, err := c.GetRoleByName(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users := getUsersFromUserArgs(c, args[1:])
|
||||
|
||||
var errs *multierror.Error
|
||||
for i, user := range users {
|
||||
if user == nil {
|
||||
printer.PrintError("Couldn't find user '" + args[i+1] + "'.")
|
||||
errs = multierror.Append(errs, fmt.Errorf("couldn't find user '%s'", args[i+1]))
|
||||
continue
|
||||
}
|
||||
|
||||
var userHasRequestedRole bool
|
||||
startingRoles := strings.Fields(user.Roles)
|
||||
for _, roleName := range startingRoles {
|
||||
if roleName == role.Name {
|
||||
userHasRequestedRole = true
|
||||
}
|
||||
}
|
||||
|
||||
if userHasRequestedRole {
|
||||
continue
|
||||
}
|
||||
|
||||
userRoles := startingRoles
|
||||
userRoles = append(userRoles, role.Name)
|
||||
_, err = c.UpdateUserRoles(context.TODO(), user.Id, strings.Join(userRoles, " "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
func unassignUsersCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
users := getUsersFromUserArgs(c, args[1:])
|
||||
|
||||
for i, user := range users {
|
||||
if user == nil {
|
||||
printer.PrintError("Couldn't find user '" + args[i+1] + "'.")
|
||||
continue
|
||||
}
|
||||
|
||||
userRoles := strings.Fields(user.Roles)
|
||||
originalCount := len(userRoles)
|
||||
|
||||
for j := 0; j < len(userRoles); j++ {
|
||||
if userRoles[j] == args[0] {
|
||||
userRoles = append(userRoles[:j], userRoles[j+1:]...)
|
||||
j--
|
||||
}
|
||||
}
|
||||
|
||||
if originalCount > len(userRoles) {
|
||||
_, err := c.UpdateUserRoles(context.TODO(), user.Id, strings.Join(userRoles, " "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
104
server/cmd/mmctl/commands/permissions_role_e2e_test.go
Normal file
104
server/cmd/mmctl/commands/permissions_role_e2e_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestAssignUsersCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.Run("MM-T3721 Should not allow normal user to assign a role", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := assignUsersCmdF(s.th.Client, &cobra.Command{}, []string{model.SystemAdminRoleId, user.Email})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3722 Assigning a user to a non-existent role", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := assignUsersCmdF(c, &cobra.Command{}, []string{"not_a_role", user.Email})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Assigning non existen user to a role", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := assignUsersCmdF(c, &cobra.Command{}, []string{model.SystemManagerRoleId, "non_existent_user"})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "Couldn't find user 'non_existent_user'.")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3648 Assigning a user to a role", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := assignUsersCmdF(c, &cobra.Command{}, []string{model.SystemManagerRoleId, user.Email})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
roles := user.Roles
|
||||
|
||||
u, err2 := s.th.App.GetUser(s.th.Context, user.Id)
|
||||
s.Require().Nil(err2)
|
||||
s.Require().True(u.IsInRole(model.SystemManagerRoleId))
|
||||
|
||||
_, err2 = s.th.App.UpdateUserRoles(s.th.Context, user.Id, roles, false)
|
||||
s.Require().Nil(err2)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestUnassignUsersCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
user, appErr := s.th.App.CreateUser(s.th.Context, &model.User{Email: s.th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
s.Run("MM-T3965 Should not allow normal user to unassign a user from a role", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := unassignUsersCmdF(s.th.Client, &cobra.Command{}, []string{model.SystemAdminRoleId, s.th.SystemAdminUser.Email})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3964 Unassign a user from a role", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
user.Roles = user.Roles + "," + model.SystemManagerRoleId
|
||||
_, appErr = s.th.App.UpdateUser(s.th.Context, user, false)
|
||||
s.Require().Nil(appErr)
|
||||
defer func() {
|
||||
user.Roles = model.SystemUserRoleId
|
||||
_, appErr := s.th.App.UpdateUser(s.th.Context, user, false)
|
||||
s.Require().Nil(appErr)
|
||||
}()
|
||||
|
||||
err := unassignUsersCmdF(c, &cobra.Command{}, []string{model.SystemManagerRoleId, user.Email})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
u, err2 := s.th.App.GetUser(s.th.Context, user.Id)
|
||||
s.Require().Nil(err2)
|
||||
s.Require().False(u.IsInRole(model.SystemManagerRoleId))
|
||||
})
|
||||
}
|
||||
277
server/cmd/mmctl/commands/permissions_test.go
Normal file
277
server/cmd/mmctl/commands/permissions_test.go
Normal file
@@ -0,0 +1,277 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestAddPermissionsCmd() {
|
||||
s.Run("Adding a new permission to an existing role", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit"},
|
||||
}
|
||||
newPermission := "delete"
|
||||
|
||||
expectedPermissions := mockRole.Permissions
|
||||
expectedPermissions = append(expectedPermissions, newPermission)
|
||||
expectedPatch := &model.RolePatch{
|
||||
Permissions: &expectedPermissions,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchRole(context.TODO(), mockRole.Id, expectedPatch).
|
||||
Return(&model.Role{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name, newPermission}
|
||||
err := addPermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Trying to add a new permission to a non existing role", func() {
|
||||
expectedError := errors.New("role_not_found")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), gomock.Any()).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusNotFound}, expectedError).
|
||||
Times(1)
|
||||
|
||||
args := []string{"mockRole", "newPermission"}
|
||||
err := addPermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Equal(expectedError, err)
|
||||
})
|
||||
|
||||
s.Run("Adding a new sysconsole_* permission to a role", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{},
|
||||
}
|
||||
newPermission := "sysconsole_read_user_management_channels"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.Run("with ancillary permissions", func() {
|
||||
expectedPermissions := mockRole.Permissions
|
||||
expectedPermissions = append(expectedPermissions, []string{newPermission, "read_public_channel", "read_channel", "read_public_channel_groups", "read_private_channel_groups"}...)
|
||||
expectedPatch := &model.RolePatch{
|
||||
Permissions: &expectedPermissions,
|
||||
}
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchRole(context.TODO(), mockRole.Id, expectedPatch).
|
||||
Return(&model.Role{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
args := []string{mockRole.Name, newPermission}
|
||||
cmd := &cobra.Command{}
|
||||
err := addPermissionsCmdF(s.client, cmd, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestRemovePermissionsCmd() {
|
||||
s.Run("Removing a permission from an existing role", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit", "delete"},
|
||||
}
|
||||
|
||||
expectedPatch := &model.RolePatch{
|
||||
Permissions: &[]string{"view", "edit"},
|
||||
}
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchRole(context.TODO(), mockRole.Id, expectedPatch).
|
||||
Return(&model.Role{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name, "delete"}
|
||||
err := removePermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Removing multiple permissions from an existing role", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit", "delete"},
|
||||
}
|
||||
|
||||
expectedPatch := &model.RolePatch{
|
||||
Permissions: &[]string{"edit"},
|
||||
}
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchRole(context.TODO(), mockRole.Id, expectedPatch).
|
||||
Return(&model.Role{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name, "view", "delete"}
|
||||
err := removePermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Removing a non-existing permission from an existing role", func() {
|
||||
mockRole := &model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit"},
|
||||
}
|
||||
|
||||
expectedPatch := &model.RolePatch{
|
||||
Permissions: &[]string{"view", "edit"},
|
||||
}
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchRole(context.TODO(), mockRole.Id, expectedPatch).
|
||||
Return(&model.Role{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name, "delete"}
|
||||
err := removePermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("Removing a permission from a non-existing role", func() {
|
||||
mockRole := model.Role{
|
||||
Name: "exampleName",
|
||||
}
|
||||
|
||||
mockError := errors.New("role_not_found")
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusNotFound}, mockError).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name, "delete"}
|
||||
err := removePermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().EqualError(err, "role_not_found")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestResetPermissionsCmd() {
|
||||
s.Run("A non-existent role", func() {
|
||||
mockRole := model.Role{
|
||||
Name: "exampleName",
|
||||
}
|
||||
|
||||
mockError := errors.New("role_not_found")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusNotFound}, mockError).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name}
|
||||
err := resetPermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().EqualError(err, "role_not_found")
|
||||
})
|
||||
|
||||
s.Run("A role without default permissions", func() {
|
||||
mockRole := model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "mock-role",
|
||||
Permissions: []string{"view", "edit", "delete"},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(&mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name}
|
||||
err := resetPermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().EqualError(err, "no default permissions available for role")
|
||||
})
|
||||
|
||||
s.Run("Resets the permissions", func() {
|
||||
mockRole := model.Role{
|
||||
Id: "mock-id",
|
||||
Name: "channel_admin",
|
||||
Permissions: []string{"view_foos", "delete_bars"},
|
||||
}
|
||||
|
||||
expectedPermissions := []string{
|
||||
"manage_channel_roles",
|
||||
"use_group_mentions",
|
||||
"add_bookmark_public_channel",
|
||||
"edit_bookmark_public_channel",
|
||||
"delete_bookmark_public_channel",
|
||||
"order_bookmark_public_channel",
|
||||
"add_bookmark_private_channel",
|
||||
"edit_bookmark_private_channel",
|
||||
"delete_bookmark_private_channel",
|
||||
"order_bookmark_private_channel",
|
||||
"manage_public_channel_banner",
|
||||
"manage_private_channel_banner",
|
||||
"manage_channel_access_rules",
|
||||
"manage_public_channel_auto_translation",
|
||||
"manage_private_channel_auto_translation",
|
||||
"manage_private_channel_discoverability",
|
||||
"manage_channel_join_requests",
|
||||
}
|
||||
expectedPatch := &model.RolePatch{
|
||||
Permissions: &expectedPermissions,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetRoleByName(context.TODO(), mockRole.Name).
|
||||
Return(&mockRole, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
PatchRole(context.TODO(), mockRole.Id, expectedPatch).
|
||||
Return(&model.Role{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
args := []string{mockRole.Name}
|
||||
err := resetPermissionsCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
}
|
||||
207
server/cmd/mmctl/commands/plugin.go
Normal file
207
server/cmd/mmctl/commands/plugin.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var PluginCmd = &cobra.Command{
|
||||
Use: "plugin",
|
||||
Short: "Management of plugins",
|
||||
}
|
||||
|
||||
var PluginAddCmd = &cobra.Command{
|
||||
Use: "add [plugins]",
|
||||
Short: "Add plugins",
|
||||
Long: "Add plugins to your Mattermost server.",
|
||||
Example: ` plugin add hovercardexample.tar.gz pluginexample.tar.gz`,
|
||||
RunE: withClient(pluginAddCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
|
||||
var PluginInstallURLCmd = &cobra.Command{
|
||||
Use: "install-url <url>...",
|
||||
Short: "Install plugin from url",
|
||||
Long: "Supply one or multiple URLs to plugins compressed in a .tar.gz file. Plugins must be enabled in the server's config settings",
|
||||
Example: ` # You can install one plugin
|
||||
$ mmctl plugin install-url https://example.com/mattermost-plugin.tar.gz
|
||||
|
||||
# Or install multiple in one go
|
||||
$ mmctl plugin install-url https://example.com/mattermost-plugin-one.tar.gz https://example.com/mattermost-plugin-two.tar.gz`,
|
||||
RunE: withClient(pluginInstallURLCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
|
||||
var PluginDeleteCmd = &cobra.Command{
|
||||
Use: "delete [plugins]",
|
||||
Short: "Delete plugins",
|
||||
Long: "Delete previously uploaded plugins from your Mattermost server.",
|
||||
Example: ` plugin delete hovercardexample pluginexample`,
|
||||
RunE: withClient(pluginDeleteCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
|
||||
var PluginEnableCmd = &cobra.Command{
|
||||
Use: "enable [plugins]",
|
||||
Short: "Enable plugins",
|
||||
Long: "Enable plugins for use on your Mattermost server.",
|
||||
Example: ` plugin enable hovercardexample pluginexample`,
|
||||
RunE: withClient(pluginEnableCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
|
||||
var PluginDisableCmd = &cobra.Command{
|
||||
Use: "disable [plugins]",
|
||||
Short: "Disable plugins",
|
||||
Long: "Disable plugins. Disabled plugins are immediately removed from the user interface and logged out of all sessions.",
|
||||
Example: ` plugin disable hovercardexample pluginexample`,
|
||||
RunE: withClient(pluginDisableCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
|
||||
var PluginListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List plugins",
|
||||
Long: "List all enabled and disabled plugins installed on your Mattermost server.",
|
||||
Example: ` plugin list`,
|
||||
RunE: withClient(pluginListCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
PluginAddCmd.Flags().BoolP("force", "f", false, "overwrite a previously installed plugin with the same ID, if any")
|
||||
PluginInstallURLCmd.Flags().BoolP("force", "f", false, "overwrite a previously installed plugin with the same ID, if any")
|
||||
|
||||
PluginCmd.AddCommand(
|
||||
PluginAddCmd,
|
||||
PluginInstallURLCmd,
|
||||
PluginDeleteCmd,
|
||||
PluginEnableCmd,
|
||||
PluginDisableCmd,
|
||||
PluginListCmd,
|
||||
)
|
||||
RootCmd.AddCommand(PluginCmd)
|
||||
}
|
||||
|
||||
func pluginAddCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
var multiErr *multierror.Error
|
||||
|
||||
for i, plugin := range args {
|
||||
fileReader, err := os.Open(plugin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if force {
|
||||
_, _, err = c.UploadPluginForced(context.TODO(), fileReader)
|
||||
} else {
|
||||
_, _, err = c.UploadPlugin(context.TODO(), fileReader)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
printer.PrintError("Unable to add plugin: " + args[i] + ". Error: " + err.Error())
|
||||
multiErr = multierror.Append(multiErr, err)
|
||||
} else {
|
||||
printer.Print("Added plugin: " + plugin)
|
||||
}
|
||||
fileReader.Close()
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func pluginInstallURLCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
var multiErr *multierror.Error
|
||||
|
||||
for _, plugin := range args {
|
||||
manifest, _, err := c.InstallPluginFromURL(context.TODO(), plugin, force)
|
||||
if err != nil {
|
||||
printer.PrintError("Unable to install plugin from URL \"" + plugin + "\". Error: " + err.Error())
|
||||
multiErr = multierror.Append(multiErr, err)
|
||||
} else {
|
||||
printer.PrintT("Plugin {{.Name}} successfully installed", manifest)
|
||||
}
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func pluginDeleteCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
var multiErr *multierror.Error
|
||||
for _, plugin := range args {
|
||||
if _, err := c.RemovePlugin(context.TODO(), plugin); err != nil {
|
||||
printer.PrintError("Unable to delete plugin: " + plugin + ". Error: " + err.Error())
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("Unable to delete plugin. %w", err))
|
||||
} else {
|
||||
printer.Print("Deleted plugin: " + plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func pluginEnableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
var multiErr *multierror.Error
|
||||
for _, plugin := range args {
|
||||
if _, err := c.EnablePlugin(context.TODO(), plugin); err != nil {
|
||||
printer.PrintError("Unable to enable plugin: " + plugin + ". Error: " + err.Error())
|
||||
multiErr = multierror.Append(multiErr, err)
|
||||
} else {
|
||||
printer.Print("Enabled plugin: " + plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func pluginDisableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
var multiErr *multierror.Error
|
||||
for _, plugin := range args {
|
||||
if _, err := c.DisablePlugin(context.TODO(), plugin); err != nil {
|
||||
printer.PrintError("Unable to disable plugin: " + plugin + ". Error: " + err.Error())
|
||||
multiErr = multierror.Append(multiErr, err)
|
||||
} else {
|
||||
printer.Print("Disabled plugin: " + plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func pluginListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
pluginsResp, _, err := c.GetPlugins(context.TODO())
|
||||
if err != nil {
|
||||
return errors.New("Unable to list plugins. Error: " + err.Error())
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
json, _ := cmd.Flags().GetBool("json")
|
||||
if format == printer.FormatJSON || json {
|
||||
printer.Print(pluginsResp)
|
||||
} else {
|
||||
printer.Print("Listing enabled plugins")
|
||||
for _, plugin := range pluginsResp.Active {
|
||||
printer.PrintT("{{.Manifest.Id}}: {{.Manifest.Name}}, Version: {{.Manifest.Version}}", plugin)
|
||||
}
|
||||
|
||||
printer.Print("")
|
||||
printer.Print("Listing disabled plugins")
|
||||
for _, plugin := range pluginsResp.Inactive {
|
||||
printer.PrintT("{{.Manifest.Id}}: {{.Manifest.Name}}, Version: {{.Manifest.Version}}", plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
609
server/cmd/mmctl/commands/plugin_e2e_test.go
Normal file
609
server/cmd/mmctl/commands/plugin_e2e_test.go
Normal file
@@ -0,0 +1,609 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPluginAddCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
pluginPath := filepath.Join(server.GetPackagePath(), "tests", "testplugin.tar.gz")
|
||||
|
||||
s.RunForSystemAdminAndLocal("add an already installed plugin without force", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
*cfg.PluginSettings.EnableUploads = false
|
||||
})
|
||||
|
||||
err := pluginAddCmdF(c, &cobra.Command{}, []string{pluginPath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Equal(1, len(printer.GetLines()))
|
||||
s.Require().Contains(printer.GetLines()[0], "Added plugin: ")
|
||||
|
||||
printer.Clean()
|
||||
|
||||
err = pluginAddCmdF(c, &cobra.Command{}, []string{pluginPath})
|
||||
s.Require().ErrorContains(err, "Unable to install plugin. A plugin with the same ID is already installed.")
|
||||
|
||||
s.Require().Equal(0, len(printer.GetLines()))
|
||||
s.Require().Equal(1, len(printer.GetErrorLines()))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "Unable to install plugin. A plugin with the same ID is already installed.")
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
|
||||
// teardown
|
||||
pInfo := plugins.Inactive[0]
|
||||
err = pluginDeleteCmdF(c, &cobra.Command{}, []string{pInfo.Id})
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("add an already installed plugin with force", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
*cfg.PluginSettings.EnableUploads = false
|
||||
})
|
||||
|
||||
err := pluginAddCmdF(c, &cobra.Command{}, []string{pluginPath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Equal(1, len(printer.GetLines()))
|
||||
s.Require().Contains(printer.GetLines()[0], "Added plugin: ")
|
||||
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("force", true, "")
|
||||
err = pluginAddCmdF(c, cmd, []string{pluginPath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Equal(1, len(printer.GetLines()))
|
||||
s.Require().Equal(0, len(printer.GetErrorLines()))
|
||||
s.Require().Contains(printer.GetLines()[0], "Added plugin: ")
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
|
||||
// teardown
|
||||
pInfo := plugins.Inactive[0]
|
||||
err = pluginDeleteCmdF(c, &cobra.Command{}, []string{pInfo.Id})
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("admin and local can't add plugins if the config doesn't allow it", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := pluginAddCmdF(c, &cobra.Command{}, []string{pluginPath})
|
||||
s.Require().ErrorContains(err, "Plugins and/or plugin uploads have been disabled.")
|
||||
s.Require().Equal(1, len(printer.GetErrorLines()))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "Plugins and/or plugin uploads have been disabled.")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("admin and local can add a plugin if the config allows it", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
*cfg.PluginSettings.EnableUploads = false
|
||||
})
|
||||
|
||||
err := pluginAddCmdF(c, &cobra.Command{}, []string{pluginPath})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Equal(1, len(printer.GetLines()))
|
||||
s.Require().Contains(printer.GetLines()[0], "Added plugin: ")
|
||||
|
||||
res, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Equal(1, len(res.Inactive))
|
||||
|
||||
// teardown
|
||||
pInfo := res.Inactive[0]
|
||||
err = pluginDeleteCmdF(c, &cobra.Command{}, []string{pInfo.Id})
|
||||
s.Require().Nil(err)
|
||||
})
|
||||
|
||||
s.Run("normal user can't add plugin", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
*cfg.PluginSettings.EnableUploads = false
|
||||
})
|
||||
|
||||
err := pluginAddCmdF(s.th.Client, &cobra.Command{}, []string{pluginPath})
|
||||
s.Require().ErrorContains(err, "You do not have the appropriate permissions")
|
||||
s.Require().Equal(1, len(printer.GetErrorLines()))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "You do not have the appropriate permissions")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPluginInstallURLCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
testsDir := filepath.Join(server.GetPackagePath(), "tests")
|
||||
fileServer := httptest.NewServer(http.FileServer(http.Dir(testsDir)))
|
||||
defer fileServer.Close()
|
||||
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
*cfg.PluginSettings.AllowInsecureDownloadURL = true
|
||||
})
|
||||
|
||||
const (
|
||||
plugin1ID = "testplugin"
|
||||
plugin2ID = "testplugin2"
|
||||
)
|
||||
plugin1URL := fileServer.URL + "/testplugin.tar.gz"
|
||||
plugin2URL := fileServer.URL + "/testplugin2.tar.gz"
|
||||
|
||||
s.RunForSystemAdminAndLocal("install new plugins", func(c client.Client) {
|
||||
printer.Clean()
|
||||
defer removePluginIfInstalled(c, s, plugin1ID)
|
||||
defer removePluginIfInstalled(c, s, plugin2ID)
|
||||
|
||||
err := pluginInstallURLCmdF(c, &cobra.Command{}, []string{plugin1URL, plugin2URL})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(plugin1ID, printer.GetLines()[0].(*model.Manifest).Id)
|
||||
s.Require().Equal(plugin2ID, printer.GetLines()[1].(*model.Manifest).Id)
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 2)
|
||||
})
|
||||
|
||||
s.Run("install a plugin without permissions", func() {
|
||||
printer.Clean()
|
||||
defer removePluginIfInstalled(s.th.Client, s, plugin1ID)
|
||||
|
||||
var expected error
|
||||
expected = multierror.Append(expected, errors.New("You do not have the appropriate permissions.")) //nolint:revive
|
||||
err := pluginInstallURLCmdF(s.th.Client, &cobra.Command{}, []string{plugin1URL})
|
||||
s.Require().ErrorContains(err, expected.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("Unable to install plugin from URL \"%s\".", plugin1URL))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "You do not have the appropriate permissions.")
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("install a nonexistent plugin", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
nonexistentURL := fileServer.URL + "/nonexistent-plugin.tar.gz"
|
||||
var expected error
|
||||
expected = multierror.Append(expected, errors.New("An error occurred while downloading the plugin.")) //nolint:revive
|
||||
|
||||
err := pluginInstallURLCmdF(c, &cobra.Command{}, []string{nonexistentURL})
|
||||
s.Require().ErrorContains(err, expected.Error())
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("Unable to install plugin from URL \"%s\".", nonexistentURL))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "An error occurred while downloading the plugin.")
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("install an already installed plugin without force", func(c client.Client) {
|
||||
printer.Clean()
|
||||
defer removePluginIfInstalled(c, s, plugin1ID)
|
||||
|
||||
err := pluginInstallURLCmdF(c, &cobra.Command{}, []string{plugin1URL})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(plugin1ID, printer.GetLines()[0].(*model.Manifest).Id)
|
||||
|
||||
err = pluginInstallURLCmdF(c, &cobra.Command{}, []string{plugin1URL})
|
||||
s.Require().ErrorContains(err, "Unable to install plugin. A plugin with the same ID is already installed.")
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("Unable to install plugin from URL \"%s\".", plugin1URL))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "Unable to install plugin. A plugin with the same ID is already installed.")
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("install an already installed plugin with force", func(c client.Client) {
|
||||
printer.Clean()
|
||||
defer removePluginIfInstalled(c, s, plugin1ID)
|
||||
|
||||
err := pluginInstallURLCmdF(c, &cobra.Command{}, []string{plugin1URL})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(plugin1ID, printer.GetLines()[0].(*model.Manifest).Id)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("force", true, "")
|
||||
err = pluginInstallURLCmdF(c, cmd, []string{plugin1URL})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(plugin1ID, printer.GetLines()[1].(*model.Manifest).Id)
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPluginDeleteCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
testsDir := filepath.Join(server.GetPackagePath(), "tests")
|
||||
fileServer := httptest.NewServer(http.FileServer(http.Dir(testsDir)))
|
||||
defer fileServer.Close()
|
||||
|
||||
const (
|
||||
pluginID = "testplugin"
|
||||
dummyPluginID = "randompluginxz"
|
||||
)
|
||||
pluginURL := fileServer.URL + "/testplugin.tar.gz"
|
||||
|
||||
s.RunForSystemAdminAndLocal("Delete Plugin", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
*cfg.PluginSettings.AllowInsecureDownloadURL = true
|
||||
})
|
||||
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
*cfg.PluginSettings.EnableUploads = false
|
||||
*cfg.PluginSettings.AllowInsecureDownloadURL = false
|
||||
})
|
||||
|
||||
errInstall := pluginInstallURLCmdF(c, &cobra.Command{}, []string{pluginURL})
|
||||
s.Require().Nil(errInstall)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(pluginID, printer.GetLines()[0].(*model.Manifest).Id)
|
||||
|
||||
pluginsAvail, appErrInstall := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErrInstall)
|
||||
s.Require().Len(pluginsAvail.Active, 0)
|
||||
s.Require().Len(pluginsAvail.Inactive, 1)
|
||||
|
||||
err := pluginDeleteCmdF(c, &cobra.Command{}, []string{pluginID})
|
||||
s.Require().Nil(err)
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Delete Unknown Plugin", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := pluginDeleteCmdF(c, &cobra.Command{}, []string{dummyPluginID})
|
||||
s.Require().ErrorContains(err, "Plugins have been disabled.")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("Unable to delete plugin: %s.", dummyPluginID))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "Plugins have been disabled.")
|
||||
})
|
||||
|
||||
s.Run("Delete a Plugin without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
*cfg.PluginSettings.AllowInsecureDownloadURL = true
|
||||
})
|
||||
|
||||
defer func() {
|
||||
errDelete := pluginDeleteCmdF(s.th.SystemAdminClient, &cobra.Command{}, []string{pluginID})
|
||||
s.Require().Nil(errDelete)
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
*cfg.PluginSettings.EnableUploads = false
|
||||
*cfg.PluginSettings.AllowInsecureDownloadURL = false
|
||||
})
|
||||
}()
|
||||
|
||||
errInstall := pluginInstallURLCmdF(s.th.SystemAdminClient, &cobra.Command{}, []string{pluginURL})
|
||||
s.Require().Nil(errInstall)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(pluginID, printer.GetLines()[0].(*model.Manifest).Id)
|
||||
|
||||
pluginsAvail, appErrInstall := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErrInstall)
|
||||
s.Require().Len(pluginsAvail.Active, 0)
|
||||
s.Require().Len(pluginsAvail.Inactive, 1)
|
||||
|
||||
err := pluginDeleteCmdF(s.th.Client, &cobra.Command{}, []string{pluginID})
|
||||
s.Require().ErrorContains(err, "You do not have the appropriate permissions.")
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("Unable to delete plugin: %s.", pluginID))
|
||||
s.Require().Contains(printer.GetErrorLines()[0], "You do not have the appropriate permissions.")
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPluginListCmdF() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("Error when appropriate permissions are not available", func() {
|
||||
printer.Clean()
|
||||
|
||||
enablePlugin := *s.th.App.Config().PluginSettings.Enable
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = enablePlugin
|
||||
})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := pluginListCmdF(s.th.Client, cmd, []string{})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Equal("Unable to list plugins. Error: You do not have the appropriate permissions.", err.Error())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Error when plugins are disabled", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
enablePlugin := *s.th.App.Config().PluginSettings.Enable
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = enablePlugin
|
||||
})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := pluginListCmdF(c, cmd, []string{})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Equal("Unable to list plugins. Error: Plugins have been disabled. Please check your logs for details.", err.Error())
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Success when appropriate permissions are available", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
enablePlugin := *s.th.App.Config().PluginSettings.Enable
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = enablePlugin
|
||||
})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := pluginListCmdF(c, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 3)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("Success when appropriate permissions are available but prints json plugins", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
enablePlugin := *s.th.App.Config().PluginSettings.Enable
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = enablePlugin
|
||||
})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("format", "json", "")
|
||||
|
||||
err := pluginListCmdF(c, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPluginDisableCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
pluginID := "testplugin"
|
||||
pluginURL := filepath.Join(server.GetPackagePath(), "tests", "testplugin.tar.gz")
|
||||
nonExistentPluginID := "nonExistentPluginID"
|
||||
|
||||
enablePlugin := *s.th.App.Config().PluginSettings.Enable
|
||||
enableUploads := *s.th.App.Config().PluginSettings.EnableUploads
|
||||
defer func() {
|
||||
appErr := s.th.App.Channels().RemovePlugin(pluginID)
|
||||
if appErr != nil {
|
||||
s.Require().Contains(appErr.Error(), "Plugin is not installed.")
|
||||
}
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = enablePlugin
|
||||
*cfg.PluginSettings.EnableUploads = enableUploads
|
||||
})
|
||||
}()
|
||||
// Enable plugin uploads
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
// Install plugin first
|
||||
err := pluginAddCmdF(s.th.SystemAdminClient, &cobra.Command{}, []string{pluginURL})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.RunForSystemAdminAndLocal("Successful disable plugin", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
appErr := s.th.App.EnablePlugin(pluginID)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 1)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
err := pluginDisableCmdF(c, cmd, []string{pluginID})
|
||||
s.Require().Nil(err)
|
||||
|
||||
plugins, appErr = s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Disabled plugin: "+pluginID)
|
||||
})
|
||||
|
||||
s.Run("error for disable plugin due insufficient permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
appErr := s.th.App.EnablePlugin(pluginID)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 1)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
err := pluginDisableCmdF(s.th.Client, cmd, []string{pluginID})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, "You do not have the appropriate permissions.")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to disable plugin: "+pluginID+". Error: You do not have the appropriate permissions.")
|
||||
|
||||
plugins, appErr = s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 1)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
|
||||
appErr = s.th.App.DisablePlugin(pluginID)
|
||||
s.Require().Nil(appErr)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("error for disabling non existent plugin", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
err := pluginDisableCmdF(c, cmd, []string{nonExistentPluginID})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, "Plugin is not installed.")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to disable plugin: "+nonExistentPluginID+". Error: Plugin is not installed.")
|
||||
|
||||
plugins, appErr = s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("error when plugin configs are disabled", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
appErr := s.th.App.EnablePlugin(pluginID)
|
||||
s.Require().Nil(appErr)
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 1)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
|
||||
enablePlugin2 := *s.th.App.Config().PluginSettings.Enable
|
||||
defer s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = enablePlugin2
|
||||
})
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
err := pluginDisableCmdF(c, cmd, []string{pluginID})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().ErrorContains(err, "Plugins have been disabled. Please check your logs for details.")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to disable plugin: "+pluginID+". Error: Plugins have been disabled. Please check your logs for details.")
|
||||
})
|
||||
}
|
||||
120
server/cmd/mmctl/commands/plugin_marketplace.go
Normal file
120
server/cmd/mmctl/commands/plugin_marketplace.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var PluginMarketplaceCmd = &cobra.Command{
|
||||
Use: "marketplace",
|
||||
Short: "Management of marketplace plugins",
|
||||
}
|
||||
|
||||
var PluginMarketplaceInstallCmd = &cobra.Command{
|
||||
Use: "install <id>",
|
||||
Short: "Install a plugin from the marketplace",
|
||||
Long: "Installs a plugin listed in the marketplace server",
|
||||
Example: ` plugin marketplace install jitsi`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(pluginMarketplaceInstallCmdF),
|
||||
}
|
||||
|
||||
var PluginMarketplaceListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List marketplace plugins",
|
||||
Long: "Gets all plugins from the marketplace server, merging data from locally installed plugins as well as prepackaged plugins shipped with the server",
|
||||
Example: ` # You can list all the plugins
|
||||
$ mmctl plugin marketplace list --all
|
||||
|
||||
# Pagination options can be used too
|
||||
$ mmctl plugin marketplace list --page 2 --per-page 10
|
||||
|
||||
# Filtering will narrow down the search
|
||||
$ mmctl plugin marketplace list --filter jit
|
||||
|
||||
# You can only retrieve local plugins
|
||||
$ mmctl plugin marketplace list --local-only`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(pluginMarketplaceListCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
PluginMarketplaceListCmd.Flags().Int("page", 0, "Page number to fetch for the list of users")
|
||||
PluginMarketplaceListCmd.Flags().Int("per-page", DefaultPageSize, "Number of users to be fetched")
|
||||
PluginMarketplaceListCmd.Flags().Bool("all", false, "Fetch all plugins. --page flag will be ignore if provided")
|
||||
PluginMarketplaceListCmd.Flags().String("filter", "", "Filter plugins by ID, name or description")
|
||||
PluginMarketplaceListCmd.Flags().Bool("local-only", false, "Only retrieve local plugins")
|
||||
|
||||
PluginMarketplaceCmd.AddCommand(
|
||||
PluginMarketplaceInstallCmd,
|
||||
PluginMarketplaceListCmd,
|
||||
)
|
||||
|
||||
PluginCmd.AddCommand(
|
||||
PluginMarketplaceCmd,
|
||||
)
|
||||
}
|
||||
|
||||
func pluginMarketplaceInstallCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
||||
id := args[0]
|
||||
|
||||
pluginRequest := &model.InstallMarketplacePluginRequest{Id: id}
|
||||
manifest, _, err := c.InstallMarketplacePlugin(context.TODO(), pluginRequest)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "couldn't install plugin from marketplace")
|
||||
}
|
||||
|
||||
printer.PrintT("Plugin {{.Name}} successfully installed", manifest)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginMarketplaceListCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
||||
page, _ := cmd.Flags().GetInt("page")
|
||||
perPage, _ := cmd.Flags().GetInt("per-page")
|
||||
showAll, _ := cmd.Flags().GetBool("all")
|
||||
filter, _ := cmd.Flags().GetString("filter")
|
||||
localOnly, _ := cmd.Flags().GetBool("local-only")
|
||||
|
||||
if showAll {
|
||||
page = 0
|
||||
}
|
||||
|
||||
for {
|
||||
pluginFilter := &model.MarketplacePluginFilter{
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
Filter: filter,
|
||||
LocalOnly: localOnly,
|
||||
}
|
||||
|
||||
plugins, _, err := c.GetMarketplacePlugins(context.TODO(), pluginFilter)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to fetch plugins")
|
||||
}
|
||||
if len(plugins) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
printer.PrintT("{{.Manifest.Id}}: {{.Manifest.Name}}, Version: {{.Manifest.Version}}", plugin)
|
||||
}
|
||||
|
||||
if !showAll {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
153
server/cmd/mmctl/commands/plugin_marketplace_e2e_test.go
Normal file
153
server/cmd/mmctl/commands/plugin_marketplace_e2e_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPluginMarketplaceInstallCmd() {
|
||||
testsPath, found := fileutils.FindDir("tests")
|
||||
s.Require().True(found, "failed to find tests directory")
|
||||
|
||||
prepackagedPluginsDir := "prepackaged_plugins"
|
||||
os.RemoveAll(prepackagedPluginsDir)
|
||||
err := os.Mkdir(prepackagedPluginsDir, os.ModePerm)
|
||||
s.Require().NoError(err)
|
||||
s.T().Cleanup(func() { os.RemoveAll(prepackagedPluginsDir) })
|
||||
|
||||
ppDir, found := fileutils.FindDir(prepackagedPluginsDir)
|
||||
s.Require().True(found, "failed to find prepackaged plugins directory")
|
||||
|
||||
err = testlib.CopyFile(filepath.Join(testsPath, "testplugin.tar.gz"), filepath.Join(ppDir, "testplugin.tar.gz"))
|
||||
s.Require().NoError(err)
|
||||
err = testlib.CopyFile(filepath.Join(testsPath, "testplugin.tar.gz.asc"), filepath.Join(ppDir, "testplugin.tar.gz.sig"))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.SetupTestHelperWithConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = []string{
|
||||
filepath.Join(testsPath, "development-public-key.asc"),
|
||||
}
|
||||
}).InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("install a plugin", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
marketPlacePlugins, appErr := s.th.App.GetMarketplacePlugins(s.th.Context, &model.MarketplacePluginFilter{
|
||||
Page: 0,
|
||||
PerPage: 100,
|
||||
Filter: "testplugin",
|
||||
})
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().NotEmpty(marketPlacePlugins)
|
||||
plugin := marketPlacePlugins[0]
|
||||
|
||||
pluginID := plugin.Manifest.Id
|
||||
pluginVersion := plugin.Manifest.Version
|
||||
|
||||
defer removePluginIfInstalled(c, s, pluginID)
|
||||
|
||||
err := pluginMarketplaceInstallCmdF(c, &cobra.Command{}, []string{pluginID})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
|
||||
manifest := printer.GetLines()[0].(*model.Manifest)
|
||||
s.Require().Equal(pluginID, manifest.Id)
|
||||
s.Require().Equal(pluginVersion, manifest.Version)
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 1)
|
||||
s.Require().Equal(pluginID, plugins.Inactive[0].Id)
|
||||
s.Require().Equal(pluginVersion, plugins.Inactive[0].Version)
|
||||
})
|
||||
|
||||
s.Run("install a plugin without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
const (
|
||||
pluginID = "testplugin"
|
||||
)
|
||||
|
||||
defer removePluginIfInstalled(s.th.Client, s, pluginID)
|
||||
|
||||
err := pluginMarketplaceInstallCmdF(s.th.Client, &cobra.Command{}, []string{pluginID})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "You do not have the appropriate permissions.")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("install a nonexistent plugin", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
const (
|
||||
pluginID = "a-nonexistent-plugin"
|
||||
)
|
||||
|
||||
defer removePluginIfInstalled(c, s, pluginID)
|
||||
|
||||
err := pluginMarketplaceInstallCmdF(c, &cobra.Command{}, []string{pluginID})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "Could not find the requested marketplace plugin")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
|
||||
plugins, appErr := s.th.App.GetPlugins()
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(plugins.Active, 0)
|
||||
s.Require().Len(plugins.Inactive, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func removePluginIfInstalled(c client.Client, s *MmctlE2ETestSuite, pluginID string) {
|
||||
appErr := pluginDeleteCmdF(c, &cobra.Command{}, []string{pluginID})
|
||||
if appErr != nil {
|
||||
s.Require().Contains(appErr.Error(), "Unable to delete plugin.")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPluginMarketplaceListCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("List Marketplace Plugins for Admin User", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := pluginMarketplaceListCmdF(c, &cobra.Command{}, nil)
|
||||
|
||||
pluginList := printer.GetLines()
|
||||
|
||||
// This checks whether there is an output from the command - returned list can be of length >= 0
|
||||
s.Require().Len(pluginList, len(pluginList))
|
||||
s.Require().NoError(err)
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
})
|
||||
|
||||
s.Run("List Marketplace Plugins for non-admin User", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := pluginMarketplaceListCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
|
||||
s.Require().ErrorContains(err, "Failed to fetch plugins: You do not have the appropriate permissions.")
|
||||
s.Require().Empty(printer.GetErrorLines())
|
||||
s.Require().Empty(printer.GetLines())
|
||||
})
|
||||
}
|
||||
168
server/cmd/mmctl/commands/plugin_marketplace_test.go
Normal file
168
server/cmd/mmctl/commands/plugin_marketplace_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func createMarketplacePlugin(name string) *model.MarketplacePlugin {
|
||||
return &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Manifest: &model.Manifest{Name: name},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPluginMarketplaceInstallCmd() {
|
||||
s.Run("Install a valid plugin", func() {
|
||||
printer.Clean()
|
||||
|
||||
id := "myplugin"
|
||||
args := []string{id}
|
||||
pluginRequest := &model.InstallMarketplacePluginRequest{Id: id}
|
||||
manifest := &model.Manifest{Name: "My Plugin", Id: id}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
InstallMarketplacePlugin(context.TODO(), pluginRequest).
|
||||
Return(manifest, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginMarketplaceInstallCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(manifest, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Install an invalid plugin", func() {
|
||||
printer.Clean()
|
||||
|
||||
id := "myplugin"
|
||||
args := []string{id}
|
||||
pluginRequest := &model.InstallMarketplacePluginRequest{Id: id}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
InstallMarketplacePlugin(context.TODO(), pluginRequest).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := pluginMarketplaceInstallCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPluginMarketplaceListCmd() {
|
||||
s.Run("List honoring pagination flags", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("page", 0, "")
|
||||
cmd.Flags().Int("per-page", 1, "")
|
||||
pluginFilter := &model.MarketplacePluginFilter{Page: 0, PerPage: 1}
|
||||
mockPlugin := createMarketplacePlugin("My Plugin")
|
||||
plugins := []*model.MarketplacePlugin{mockPlugin}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMarketplacePlugins(context.TODO(), pluginFilter).
|
||||
Return(plugins, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginMarketplaceListCmdF(s.client, cmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(mockPlugin, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("List all plugins", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("per-page", 1, "")
|
||||
cmd.Flags().Bool("all", true, "")
|
||||
mockPlugin1 := createMarketplacePlugin("My Plugin One")
|
||||
mockPlugin2 := createMarketplacePlugin("My Plugin Two")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMarketplacePlugins(context.TODO(), &model.MarketplacePluginFilter{Page: 0, PerPage: 1}).
|
||||
Return([]*model.MarketplacePlugin{mockPlugin1}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMarketplacePlugins(context.TODO(), &model.MarketplacePluginFilter{Page: 1, PerPage: 1}).
|
||||
Return([]*model.MarketplacePlugin{mockPlugin2}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMarketplacePlugins(context.TODO(), &model.MarketplacePluginFilter{Page: 2, PerPage: 1}).
|
||||
Return([]*model.MarketplacePlugin{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginMarketplaceListCmdF(s.client, cmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(mockPlugin1, printer.GetLines()[0])
|
||||
s.Require().Equal(mockPlugin2, printer.GetLines()[1])
|
||||
})
|
||||
|
||||
s.Run("List all plugins with errors", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMarketplacePlugins(context.TODO(), &model.MarketplacePluginFilter{Page: 0, PerPage: 200}).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := pluginMarketplaceListCmdF(s.client, cmd, []string{})
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List honoring filter and local only flags", func() {
|
||||
printer.Clean()
|
||||
|
||||
filter := "jit"
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("per-page", 200, "")
|
||||
cmd.Flags().String("filter", filter, "")
|
||||
cmd.Flags().Bool("local-only", true, "")
|
||||
pluginFilter := &model.MarketplacePluginFilter{Page: 0, PerPage: 200, Filter: filter, LocalOnly: true}
|
||||
mockPlugin := createMarketplacePlugin("Jitsi")
|
||||
plugins := []*model.MarketplacePlugin{mockPlugin}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMarketplacePlugins(context.TODO(), pluginFilter).
|
||||
Return(plugins, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginMarketplaceListCmdF(s.client, cmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(mockPlugin, printer.GetLines()[0])
|
||||
})
|
||||
}
|
||||
619
server/cmd/mmctl/commands/plugin_test.go
Normal file
619
server/cmd/mmctl/commands/plugin_test.go
Normal file
@@ -0,0 +1,619 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPluginAddCmd() {
|
||||
s.Run("Add 1 plugin", func() {
|
||||
printer.Clean()
|
||||
tmpFile, err := os.CreateTemp("", "tmpPlugin")
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
pluginName := tmpFile.Name()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadPlugin(context.TODO(), gomock.AssignableToTypeOf(tmpFile)).
|
||||
Return(&model.Manifest{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = pluginAddCmdF(s.client, &cobra.Command{}, []string{pluginName})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Added plugin: "+pluginName)
|
||||
})
|
||||
|
||||
s.Run("Add 1 plugin, with force active", func() {
|
||||
printer.Clean()
|
||||
tmpFile, err := os.CreateTemp("", "tmpPlugin")
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
pluginName := tmpFile.Name()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadPluginForced(context.TODO(), gomock.AssignableToTypeOf(tmpFile)).
|
||||
Return(&model.Manifest{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("force", true, "")
|
||||
|
||||
err = pluginAddCmdF(s.client, cmd, []string{pluginName})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Added plugin: "+pluginName)
|
||||
})
|
||||
|
||||
s.Run("Add 1 plugin no file", func() {
|
||||
printer.Clean()
|
||||
err := pluginAddCmdF(s.client, &cobra.Command{}, []string{"non_existent_plugin"})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().True(err.Error() == "open non_existent_plugin: no such file or directory" || err.Error() == "open non_existent_plugin: The system cannot find the file specified.")
|
||||
})
|
||||
|
||||
s.Run("Add 1 plugin with error", func() {
|
||||
printer.Clean()
|
||||
tmpFile, err := os.CreateTemp("", "tmpPlugin")
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
pluginName := tmpFile.Name()
|
||||
mockError := errors.New("plugin add error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadPlugin(context.TODO(), gomock.AssignableToTypeOf(tmpFile)).
|
||||
Return(&model.Manifest{}, &model.Response{}, mockError).
|
||||
Times(1)
|
||||
|
||||
err = pluginAddCmdF(s.client, &cobra.Command{}, []string{pluginName})
|
||||
s.Require().ErrorContains(err, "plugin add error")
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to add plugin: "+pluginName+". Error: "+mockError.Error())
|
||||
})
|
||||
|
||||
s.Run("Add several plugins with some error", func() {
|
||||
printer.Clean()
|
||||
args := []string{"fail", "ok", "fail"}
|
||||
mockError := errors.New("plugin add error")
|
||||
|
||||
for idx, arg := range args {
|
||||
tmpFile, err := os.CreateTemp("", "tmpPlugin")
|
||||
s.Require().Nil(err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
if arg == "fail" {
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadPlugin(context.TODO(), gomock.AssignableToTypeOf(tmpFile)).
|
||||
Return(nil, &model.Response{}, mockError).
|
||||
Times(1)
|
||||
} else {
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadPlugin(context.TODO(), gomock.AssignableToTypeOf(tmpFile)).
|
||||
Return(&model.Manifest{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
}
|
||||
args[idx] = tmpFile.Name()
|
||||
}
|
||||
|
||||
err := pluginAddCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().ErrorContains(err, "plugin add error")
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Added plugin: "+args[1])
|
||||
s.Require().Len(printer.GetErrorLines(), 2)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to add plugin: "+args[0]+". Error: "+mockError.Error())
|
||||
s.Require().Equal(printer.GetErrorLines()[1], "Unable to add plugin: "+args[2]+". Error: "+mockError.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPluginInstallUrlCmd() {
|
||||
s.Run("Install multiple plugins", func() {
|
||||
printer.Clean()
|
||||
|
||||
pluginURL1 := "https://example.com/plugin1.tar.gz"
|
||||
pluginURL2 := "https://example.com/plugin2.tar.gz"
|
||||
manifest1 := &model.Manifest{Name: "plugin one"}
|
||||
manifest2 := &model.Manifest{Name: "plugin two"}
|
||||
args := []string{pluginURL1, pluginURL2}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
InstallPluginFromURL(context.TODO(), pluginURL1, false).
|
||||
Return(manifest1, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
InstallPluginFromURL(context.TODO(), pluginURL2, false).
|
||||
Return(manifest2, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginInstallURLCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(manifest1, printer.GetLines()[0])
|
||||
s.Require().Equal(manifest2, printer.GetLines()[1])
|
||||
})
|
||||
|
||||
s.Run("Install one plugin, with force active", func() {
|
||||
printer.Clean()
|
||||
|
||||
pluginURL := "https://example.com/plugin.tar.gz"
|
||||
manifest := &model.Manifest{Name: "plugin name"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
InstallPluginFromURL(context.TODO(), pluginURL, true).
|
||||
Return(manifest, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("force", true, "")
|
||||
|
||||
err := pluginInstallURLCmdF(s.client, cmd, []string{pluginURL})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(manifest, printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Install multiple plugins, some failing", func() {
|
||||
printer.Clean()
|
||||
|
||||
pluginURL1 := "https://example.com/plugin1.tar.gz"
|
||||
pluginURL2 := "https://example.com/plugin2.tar.gz"
|
||||
manifest1 := &model.Manifest{Name: "plugin one"}
|
||||
args := []string{pluginURL1, pluginURL2}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
InstallPluginFromURL(context.TODO(), pluginURL1, false).
|
||||
Return(manifest1, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
InstallPluginFromURL(context.TODO(), pluginURL2, false).
|
||||
Return(nil, &model.Response{}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := pluginInstallURLCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().ErrorContains(err, "mock error")
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal("Unable to install plugin from URL \"https://example.com/plugin2.tar.gz\". Error: mock error", printer.GetErrorLines()[0])
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(manifest1, printer.GetLines()[0])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPluginDisableCmd() {
|
||||
s.Run("Disable 1 plugin", func() {
|
||||
printer.Clean()
|
||||
arg := "plug1"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DisablePlugin(context.TODO(), arg).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginDisableCmdF(s.client, &cobra.Command{}, []string{arg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Disabled plugin: "+arg)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Fail to disable 1 plugin", func() {
|
||||
printer.Clean()
|
||||
arg := "fail1"
|
||||
mockError := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DisablePlugin(context.TODO(), arg).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockError).
|
||||
Times(1)
|
||||
|
||||
err := pluginDisableCmdF(s.client, &cobra.Command{}, []string{arg})
|
||||
s.Require().ErrorContains(err, "mock error")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to disable plugin: "+arg+". Error: "+mockError.Error())
|
||||
})
|
||||
|
||||
s.Run("Disble several plugin with some errors", func() {
|
||||
printer.Clean()
|
||||
args := []string{"fail1", "plug2", "plug3", "fail4"}
|
||||
mockError := errors.New("mock error")
|
||||
|
||||
for _, arg := range args {
|
||||
if strings.HasPrefix(arg, "fail") {
|
||||
s.client.
|
||||
EXPECT().
|
||||
DisablePlugin(context.TODO(), arg).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockError).
|
||||
Times(1)
|
||||
} else {
|
||||
s.client.
|
||||
EXPECT().
|
||||
DisablePlugin(context.TODO(), arg).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, nil).
|
||||
Times(1)
|
||||
}
|
||||
}
|
||||
|
||||
err := pluginDisableCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().ErrorContains(err, "mock error")
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(printer.GetLines()[0], "Disabled plugin: "+args[1])
|
||||
s.Require().Equal(printer.GetLines()[1], "Disabled plugin: "+args[2])
|
||||
s.Require().Len(printer.GetErrorLines(), 2)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to disable plugin: "+args[0]+". Error: "+mockError.Error())
|
||||
s.Require().Equal(printer.GetErrorLines()[1], "Unable to disable plugin: "+args[3]+". Error: "+mockError.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPluginEnableCmd() {
|
||||
s.Run("Enable 1 plugin", func() {
|
||||
printer.Clean()
|
||||
pluginArg := "test-plugin"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
EnablePlugin(context.TODO(), pluginArg).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginEnableCmdF(s.client, &cobra.Command{}, []string{pluginArg})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], "Enabled plugin: "+pluginArg)
|
||||
})
|
||||
|
||||
s.Run("Enable multiple plugins", func() {
|
||||
printer.Clean()
|
||||
plugins := []string{"plugin1", "plugin2", "plugin3"}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
s.client.
|
||||
EXPECT().
|
||||
EnablePlugin(context.TODO(), plugin).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, nil).
|
||||
Times(1)
|
||||
}
|
||||
|
||||
err := pluginEnableCmdF(s.client, &cobra.Command{}, plugins)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 3)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(printer.GetLines()[0], "Enabled plugin: "+plugins[0])
|
||||
s.Require().Equal(printer.GetLines()[1], "Enabled plugin: "+plugins[1])
|
||||
s.Require().Equal(printer.GetLines()[2], "Enabled plugin: "+plugins[2])
|
||||
})
|
||||
|
||||
s.Run("Fail to enable plugin", func() {
|
||||
printer.Clean()
|
||||
pluginArg := "fail-plugin"
|
||||
mockErr := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
EnablePlugin(context.TODO(), pluginArg).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockErr).
|
||||
Times(1)
|
||||
|
||||
err := pluginEnableCmdF(s.client, &cobra.Command{}, []string{pluginArg})
|
||||
s.Require().ErrorContains(err, "mock error")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to enable plugin: "+pluginArg+". Error: "+mockErr.Error())
|
||||
})
|
||||
|
||||
s.Run("Enable multiple plugins with some having errors", func() {
|
||||
printer.Clean()
|
||||
okPlugins := []string{"ok-plugin-1", "ok-plugin-2"}
|
||||
failPlugins := []string{"fail-plugin-1", "fail-plugin-2"}
|
||||
allPlugins := okPlugins
|
||||
allPlugins = append(allPlugins, failPlugins...)
|
||||
|
||||
mockErr := errors.New("mock error")
|
||||
|
||||
for _, plugin := range okPlugins {
|
||||
s.client.
|
||||
EXPECT().
|
||||
EnablePlugin(context.TODO(), plugin).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, nil).
|
||||
Times(1)
|
||||
}
|
||||
|
||||
for _, plugin := range failPlugins {
|
||||
s.client.
|
||||
EXPECT().
|
||||
EnablePlugin(context.TODO(), plugin).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockErr).
|
||||
Times(1)
|
||||
}
|
||||
|
||||
err := pluginEnableCmdF(s.client, &cobra.Command{}, allPlugins)
|
||||
s.Require().ErrorContains(err, "mock error")
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(printer.GetLines()[0], "Enabled plugin: "+okPlugins[0])
|
||||
s.Require().Equal(printer.GetLines()[1], "Enabled plugin: "+okPlugins[1])
|
||||
s.Require().Len(printer.GetErrorLines(), 2)
|
||||
s.Require().Equal(printer.GetErrorLines()[0], "Unable to enable plugin: "+failPlugins[0]+". Error: "+mockErr.Error())
|
||||
s.Require().Equal(printer.GetErrorLines()[1], "Unable to enable plugin: "+failPlugins[1]+". Error: "+mockErr.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPluginListCmd() {
|
||||
s.Run("List JSON plugins", func() {
|
||||
printer.Clean()
|
||||
mockList := &model.PluginsResponse{
|
||||
Active: []*model.PluginInfo{
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id1",
|
||||
Name: "name1",
|
||||
Version: "v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id2",
|
||||
Name: "name2",
|
||||
Version: "v2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id3",
|
||||
Name: "name3",
|
||||
Version: "v3",
|
||||
},
|
||||
},
|
||||
}, Inactive: []*model.PluginInfo{
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id4",
|
||||
Name: "name4",
|
||||
Version: "v4",
|
||||
},
|
||||
},
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id5",
|
||||
Name: "name5",
|
||||
Version: "v5",
|
||||
},
|
||||
},
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id6",
|
||||
Name: "name6",
|
||||
Version: "v6",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPlugins(context.TODO()).
|
||||
Return(mockList, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginListCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 9)
|
||||
|
||||
s.Require().Equal("Listing enabled plugins", printer.GetLines()[0])
|
||||
for i, plugin := range mockList.Active {
|
||||
s.Require().Equal(plugin, printer.GetLines()[i+1])
|
||||
}
|
||||
|
||||
s.Require().Equal("", printer.GetLines()[4])
|
||||
s.Require().Equal("Listing disabled plugins", printer.GetLines()[5])
|
||||
for i, plugin := range mockList.Inactive {
|
||||
s.Require().Equal(plugin, printer.GetLines()[i+6])
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("List Plain Plugins", func() {
|
||||
printer.Clean()
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
defer printer.SetFormat(printer.FormatJSON)
|
||||
|
||||
mockList := &model.PluginsResponse{
|
||||
Active: []*model.PluginInfo{
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id1",
|
||||
Name: "name1",
|
||||
Version: "v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id2",
|
||||
Name: "name2",
|
||||
Version: "v2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id3",
|
||||
Name: "name3",
|
||||
Version: "v3",
|
||||
},
|
||||
},
|
||||
}, Inactive: []*model.PluginInfo{
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id4",
|
||||
Name: "name4",
|
||||
Version: "v4",
|
||||
},
|
||||
},
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id5",
|
||||
Name: "name5",
|
||||
Version: "v5",
|
||||
},
|
||||
},
|
||||
{
|
||||
Manifest: model.Manifest{
|
||||
Id: "id6",
|
||||
Name: "name6",
|
||||
Version: "v6",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPlugins(context.TODO()).
|
||||
Return(mockList, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginListCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 9)
|
||||
|
||||
s.Require().Equal("Listing enabled plugins", printer.GetLines()[0])
|
||||
for i, plugin := range mockList.Active {
|
||||
s.Require().Equal(plugin.Id+": "+plugin.Name+", Version: "+plugin.Version, printer.GetLines()[i+1])
|
||||
}
|
||||
|
||||
s.Require().Equal("", printer.GetLines()[4])
|
||||
s.Require().Equal("Listing disabled plugins", printer.GetLines()[5])
|
||||
for i, plugin := range mockList.Inactive {
|
||||
s.Require().Equal(plugin.Id+": "+plugin.Name+", Version: "+plugin.Version, printer.GetLines()[i+6])
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("GetPlugins returns error", func() {
|
||||
printer.Clean()
|
||||
mockError := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPlugins(context.TODO()).
|
||||
Return(nil, &model.Response{}, mockError).
|
||||
Times(1)
|
||||
|
||||
err := pluginListCmdF(s.client, &cobra.Command{}, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().EqualError(err, "Unable to list plugins. Error: "+mockError.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPluginDeleteCmd() {
|
||||
s.Run("Delete one plugin with error", func() {
|
||||
printer.Clean()
|
||||
args := "plugin"
|
||||
mockError := errors.New("mock error")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemovePlugin(context.TODO(), args).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockError).
|
||||
Times(1)
|
||||
|
||||
err := pluginDeleteCmdF(s.client, &cobra.Command{}, []string{args})
|
||||
s.Require().ErrorContains(err, "mock error")
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal("Unable to delete plugin: "+args+". Error: "+mockError.Error(), printer.GetErrorLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Delete one plugin with no error", func() {
|
||||
printer.Clean()
|
||||
args := "plugin"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemovePlugin(context.TODO(), args).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginDeleteCmdF(s.client, &cobra.Command{}, []string{args})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal("Deleted plugin: "+args, printer.GetLines()[0])
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Delete several plugins", func() {
|
||||
printer.Clean()
|
||||
args := []string{
|
||||
"plugin0",
|
||||
"error1",
|
||||
"error2",
|
||||
"plugin3",
|
||||
}
|
||||
mockErrors := []error{
|
||||
errors.New("mock error1"),
|
||||
errors.New("mock error2"),
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemovePlugin(context.TODO(), args[0]).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemovePlugin(context.TODO(), args[1]).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockErrors[0]).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemovePlugin(context.TODO(), args[2]).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockErrors[1]).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RemovePlugin(context.TODO(), args[3]).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := pluginDeleteCmdF(s.client, &cobra.Command{}, args)
|
||||
s.Require().ErrorContains(err, "mock error")
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal("Deleted plugin: "+args[0], printer.GetLines()[0])
|
||||
s.Require().Equal("Deleted plugin: "+args[3], printer.GetLines()[1])
|
||||
s.Require().Len(printer.GetErrorLines(), 2)
|
||||
s.Require().Equal("Unable to delete plugin: "+args[1]+". Error: "+mockErrors[0].Error(), printer.GetErrorLines()[0])
|
||||
s.Require().Equal("Unable to delete plugin: "+args[2]+". Error: "+mockErrors[1].Error(), printer.GetErrorLines()[1])
|
||||
})
|
||||
}
|
||||
446
server/cmd/mmctl/commands/post.go
Normal file
446
server/cmd/mmctl/commands/post.go
Normal file
@@ -0,0 +1,446 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var PostCmd = &cobra.Command{
|
||||
Use: "post",
|
||||
Short: "Management of posts",
|
||||
}
|
||||
|
||||
var PostCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a post",
|
||||
Long: "Create a post in a channel or send a direct message to a user by prefixing the user with '@'.",
|
||||
Example: ` post create myteam:mychannel --message "some text for the post"
|
||||
post create @target-user --message "some text for the direct message"
|
||||
post create myteam:mychannel --message "see attachment" --file ./report.pdf --file ./image.png`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(postCreateCmdF),
|
||||
}
|
||||
|
||||
var PostListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List posts for a channel",
|
||||
Example: ` post list myteam:mychannel
|
||||
post list myteam:mychannel --number 20`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(postListCmdF),
|
||||
}
|
||||
|
||||
var PostRevealCmd = &cobra.Command{
|
||||
Use: "reveal [post]",
|
||||
Short: "Reveal a post",
|
||||
Example: ` post reveal udjmt396tjghi8wnsk3a1qs1sw`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(revealPostCmdF),
|
||||
}
|
||||
|
||||
var PostDeleteCmd = &cobra.Command{
|
||||
Use: "delete [posts]",
|
||||
Short: "Mark posts as deleted or permanently delete posts with the --permanent flag",
|
||||
Long: `This command will mark the post as deleted and remove it from the user's clients, but it does not permanently delete the post from the database. Please use the --permanent flag to permanently delete a post and its attachments from your database.`,
|
||||
Example: ` # Mark Post as deleted
|
||||
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw
|
||||
|
||||
# Permanently delete a post and it's file contents from the database and filestore
|
||||
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw --permanent
|
||||
|
||||
# Permanently delete multiple posts and their file contents from the database and filestore
|
||||
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw 7jgcjt7tyjyyu83qz81wo84w6o --permanent`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: withClient(deletePostsCmdF),
|
||||
}
|
||||
|
||||
const (
|
||||
ISO8601Layout = "2006-01-02T15:04:05-07:00"
|
||||
PostTimeFormat = "2006-01-02 15:04:05-07:00"
|
||||
|
||||
directMessagePrefix = "@"
|
||||
)
|
||||
|
||||
func init() {
|
||||
PostCreateCmd.Flags().StringP("message", "m", "", "Message for the post")
|
||||
PostCreateCmd.Flags().StringP("reply-to", "r", "", "Post id to reply to")
|
||||
PostCreateCmd.Flags().BoolP("burn-on-read", "b", false, "Message will be deleted after a certain time after being read")
|
||||
PostCreateCmd.Flags().StringArrayP("file", "f", nil, "Path to a local file to attach to the post. Can be specified multiple times to attach multiple files")
|
||||
|
||||
PostListCmd.Flags().IntP("number", "n", 20, "Number of messages to list")
|
||||
PostListCmd.Flags().BoolP("show-ids", "i", false, "Show posts ids")
|
||||
PostListCmd.Flags().BoolP("follow", "f", false, "Output appended data as new messages are posted to the channel")
|
||||
PostListCmd.Flags().StringP("since", "s", "", "List messages posted after a certain time (ISO 8601)")
|
||||
|
||||
PostDeleteCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the post and a DB backup has been performed")
|
||||
PostDeleteCmd.Flags().Bool("permanent", false, "Permanently delete the post and its contents from the database")
|
||||
|
||||
PostCmd.AddCommand(
|
||||
PostCreateCmd,
|
||||
PostListCmd,
|
||||
PostDeleteCmd,
|
||||
PostRevealCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(PostCmd)
|
||||
}
|
||||
|
||||
func postCreateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
if viper.GetBool("local") {
|
||||
return errors.New("creating posts is not supported in local mode")
|
||||
}
|
||||
|
||||
message, _ := cmd.Flags().GetString("message")
|
||||
files, _ := cmd.Flags().GetStringArray("file")
|
||||
if message == "" && len(files) == 0 {
|
||||
return errors.New("a post must have a message or at least one file attachment")
|
||||
}
|
||||
|
||||
replyTo, _ := cmd.Flags().GetString("reply-to")
|
||||
if replyTo != "" {
|
||||
replyToPost, _, err := c.GetPost(context.TODO(), replyTo, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if replyToPost.RootId != "" {
|
||||
replyTo = replyToPost.RootId
|
||||
}
|
||||
}
|
||||
|
||||
channelID, err := getPostChannelID(c, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileIDs, uploadErr, err := uploadPostFiles(c, channelID, files)
|
||||
if err != nil {
|
||||
// A file could not be opened, so nothing was uploaded and no
|
||||
// attachments were orphaned. Abort before creating the post.
|
||||
return err
|
||||
}
|
||||
if message == "" && len(fileIDs) == 0 {
|
||||
if uploadErr != nil {
|
||||
return uploadErr
|
||||
}
|
||||
return errors.New("a post must have a message or at least one file attachment")
|
||||
}
|
||||
|
||||
post := &model.Post{
|
||||
ChannelId: channelID,
|
||||
Message: message,
|
||||
RootId: replyTo,
|
||||
FileIds: fileIDs,
|
||||
}
|
||||
|
||||
if burnOnRead, _ := cmd.Flags().GetBool("burn-on-read"); burnOnRead {
|
||||
post.Type = model.PostTypeBurnOnRead
|
||||
}
|
||||
|
||||
url := "/posts" + "?set_online=false"
|
||||
data, err := post.ToJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not decode post: %w", err)
|
||||
}
|
||||
|
||||
if _, err := c.DoAPIPost(context.TODO(), url, data); err != nil {
|
||||
return fmt.Errorf("could not create post: %w", err)
|
||||
}
|
||||
|
||||
// Any files that uploaded successfully are attached to the post above, so
|
||||
// a partial batch failure never leaves those uploads orphaned. The error is
|
||||
// returned last so the caller still learns which files could not be sent.
|
||||
return uploadErr
|
||||
}
|
||||
|
||||
// uploadPostFiles streams each local file to the server and returns the IDs of
|
||||
// the files that were uploaded successfully. All files are opened up front so
|
||||
// an unreadable path (returned as the final error) fails before any upload
|
||||
// happens, leaving nothing orphaned. Once uploads start, a failure on one file
|
||||
// does not abort the batch: it is collected in uploadErr while the files that
|
||||
// did upload are still returned so they can be attached to the post.
|
||||
func uploadPostFiles(c client.Client, channelID string, files []string) (fileIDs []string, uploadErr error, err error) {
|
||||
if len(files) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
handles := make([]*os.File, 0, len(files))
|
||||
defer func() {
|
||||
for _, f := range handles {
|
||||
f.Close()
|
||||
}
|
||||
}()
|
||||
for _, file := range files {
|
||||
f, openErr := os.Open(file)
|
||||
if openErr != nil {
|
||||
return nil, nil, fmt.Errorf("could not read file '%s': %w", file, openErr)
|
||||
}
|
||||
handles = append(handles, f)
|
||||
}
|
||||
|
||||
var errs *multierror.Error
|
||||
for i, f := range handles {
|
||||
fileID, uploadFileErr := uploadPostFile(c, channelID, files[i], f)
|
||||
if uploadFileErr != nil {
|
||||
errs = multierror.Append(errs, uploadFileErr)
|
||||
continue
|
||||
}
|
||||
fileIDs = append(fileIDs, fileID)
|
||||
}
|
||||
|
||||
return fileIDs, errs.ErrorOrNil(), nil
|
||||
}
|
||||
|
||||
func uploadPostFile(c client.Client, channelID, path string, f *os.File) (string, error) {
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read file '%s': %w", path, err)
|
||||
}
|
||||
|
||||
us, _, err := c.CreateUpload(context.TODO(), &model.UploadSession{
|
||||
Type: model.UploadTypeAttachment,
|
||||
ChannelId: channelID,
|
||||
Filename: filepath.Base(path),
|
||||
FileSize: info.Size(),
|
||||
UserId: "me",
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not upload file '%s': %w", path, err)
|
||||
}
|
||||
|
||||
fileInfo, _, err := c.UploadData(context.TODO(), us.Id, f)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not upload file '%s': %w", path, err)
|
||||
}
|
||||
if fileInfo == nil {
|
||||
return "", fmt.Errorf("could not upload file '%s': upload did not complete", path)
|
||||
}
|
||||
|
||||
return fileInfo.Id, nil
|
||||
}
|
||||
|
||||
func getPostChannelID(c client.Client, arg string) (string, error) {
|
||||
if username, ok := strings.CutPrefix(arg, directMessagePrefix); ok {
|
||||
channel, err := getDirectChannel(c, username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return channel.Id, nil
|
||||
}
|
||||
|
||||
channel := getChannelFromChannelArg(c, arg)
|
||||
if channel == nil {
|
||||
return "", errors.New("Unable to find channel '" + arg + "'")
|
||||
}
|
||||
return channel.Id, nil
|
||||
}
|
||||
|
||||
func getDirectChannel(c client.Client, username string) (*model.Channel, error) {
|
||||
user, err := getUserFromArg(c, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
me, _, err := c.GetMe(context.TODO(), "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not retrieve the current user: %w", err)
|
||||
}
|
||||
|
||||
channel, _, err := c.CreateDirectChannel(context.TODO(), me.Id, user.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create direct channel with '%s': %w", username, err)
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func eventDataToPost(eventData map[string]any) (*model.Post, error) {
|
||||
post := &model.Post{}
|
||||
var rawPost string
|
||||
for k, v := range eventData {
|
||||
if k == "post" {
|
||||
rawPost = v.(string)
|
||||
}
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(rawPost), &post)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func printPost(c client.Client, post *model.Post, usernames map[string]string, showIds, showTimestamp bool) {
|
||||
var username string
|
||||
|
||||
if usernames[post.UserId] != "" {
|
||||
username = usernames[post.UserId]
|
||||
} else {
|
||||
user, _, err := c.GetUser(context.TODO(), post.UserId, "")
|
||||
if err != nil {
|
||||
username = post.UserId
|
||||
} else {
|
||||
usernames[post.UserId] = user.Username
|
||||
username = user.Username
|
||||
}
|
||||
}
|
||||
|
||||
postTime := model.GetTimeForMillis(post.CreateAt)
|
||||
createdAt := postTime.Format(PostTimeFormat)
|
||||
|
||||
var templatedMessage string
|
||||
|
||||
if showTimestamp {
|
||||
templatedMessage = fmt.Sprintf("{{ if eq .Type \"burn_on_read\" }}🔥 {{ end }}\u001b[32m%s\u001b[0m \u001b[34;1m[%s]\u001b[0m {{.Message}}", createdAt, username)
|
||||
} else {
|
||||
if showIds {
|
||||
templatedMessage = fmt.Sprintf("{{ if eq .Type \"burn_on_read\" }}🔥 {{ end }}\u001b[31m%s\u001b[0m \u001b[34;1m[%s]\u001b[0m {{.Message}}", post.Id, username)
|
||||
} else {
|
||||
templatedMessage = fmt.Sprintf("{{ if eq .Type \"burn_on_read\" }}🔥 {{ end }}\u001b[34;1m[%s]\u001b[0m {{.Message}}", username)
|
||||
}
|
||||
}
|
||||
|
||||
if post.Type == model.PostTypeBurnOnRead {
|
||||
expireAt := post.Metadata.ExpireAt
|
||||
if expireAt != 0 {
|
||||
dur := time.Until(time.UnixMilli(expireAt))
|
||||
templatedMessage = fmt.Sprintf("%s (expires in %s)", templatedMessage, dur.String())
|
||||
}
|
||||
}
|
||||
printer.PrintT(templatedMessage, post)
|
||||
}
|
||||
|
||||
func getPostList(client client.Client, channelID, since string, perPage int) (*model.PostList, *model.Response, error) {
|
||||
if since == "" {
|
||||
return client.GetPostsForChannel(context.TODO(), channelID, 0, perPage, "", false, false)
|
||||
}
|
||||
|
||||
sinceTime, err := time.Parse(ISO8601Layout, since)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid since time '%s'", since)
|
||||
}
|
||||
|
||||
sinceTimeMillis := model.GetMillisForTime(sinceTime)
|
||||
return client.GetPostsSince(context.TODO(), channelID, sinceTimeMillis, false)
|
||||
}
|
||||
|
||||
func postListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.New("Unable to find channel '" + args[0] + "'")
|
||||
}
|
||||
|
||||
number, _ := cmd.Flags().GetInt("number")
|
||||
showIds, _ := cmd.Flags().GetBool("show-ids")
|
||||
follow, _ := cmd.Flags().GetBool("follow")
|
||||
since, _ := cmd.Flags().GetString("since")
|
||||
|
||||
postList, _, err := getPostList(c, channel.Id, since, number)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
posts := postList.ToSlice()
|
||||
showTimestamp := since != ""
|
||||
usernames := map[string]string{}
|
||||
for i := 1; i <= len(posts); i++ {
|
||||
post := posts[len(posts)-i]
|
||||
printPost(c, post, usernames, showIds, showTimestamp)
|
||||
}
|
||||
|
||||
var multiErr *multierror.Error
|
||||
if follow {
|
||||
ws, err := InitWebSocketClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appErr := ws.Connect()
|
||||
if appErr != nil {
|
||||
return errors.New(appErr.Error())
|
||||
}
|
||||
|
||||
ws.Listen()
|
||||
for {
|
||||
event := <-ws.EventChannel
|
||||
if event.EventType() == model.WebsocketEventPosted {
|
||||
post, err := eventDataToPost(event.GetData())
|
||||
if err != nil {
|
||||
printer.PrintError("Error parsing incoming post: " + err.Error())
|
||||
multiErr = multierror.Append(multiErr, err)
|
||||
}
|
||||
if post.ChannelId == channel.Id {
|
||||
printPost(c, post, usernames, showIds, showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func deletePostsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
permanent, err := cmd.Flags().GetBool("permanent")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confirmFlag, _ := cmd.Flags().GetBool("confirm")
|
||||
if !confirmFlag && permanent {
|
||||
if err = getConfirmation("Are you sure you want to delete the posts specified?", true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var result *multierror.Error
|
||||
var deleteFunc func(ctx context.Context, postID string) (*model.Response, error)
|
||||
|
||||
if permanent {
|
||||
deleteFunc = c.PermanentDeletePost
|
||||
} else {
|
||||
deleteFunc = c.DeletePost
|
||||
}
|
||||
|
||||
for _, postID := range args {
|
||||
isValidId := model.IsValidId(postID)
|
||||
if !isValidId {
|
||||
printer.PrintError(fmt.Sprintf("Invalid postID: %s", postID))
|
||||
result = multierror.Append(result, err)
|
||||
continue
|
||||
}
|
||||
if _, err := deleteFunc(context.TODO(), postID); err != nil {
|
||||
printer.PrintError(fmt.Sprintf("Error deleting post: %s. Error: %s", postID, err.Error()))
|
||||
result = multierror.Append(result, err)
|
||||
continue
|
||||
}
|
||||
printer.Print(fmt.Sprintf("%s successfully deleted", postID))
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func revealPostCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
postID := args[0]
|
||||
post, _, err := c.RevealPost(context.TODO(), postID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printPost(c, post, map[string]string{}, false, false)
|
||||
return nil
|
||||
}
|
||||
461
server/cmd/mmctl/commands/post_e2e_test.go
Normal file
461
server/cmd/mmctl/commands/post_e2e_test.go
Normal file
@@ -0,0 +1,461 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPostListCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
var createNewChannelAndPosts = func() (string, *model.Post, *model.Post) {
|
||||
channelName := model.NewRandomString(10)
|
||||
channelDisplayName := "channelDisplayName"
|
||||
|
||||
channel, err := s.th.App.CreateChannel(s.th.Context, &model.Channel{Name: channelName, DisplayName: channelDisplayName, Type: model.ChannelTypePrivate, TeamId: s.th.BasicTeam.Id}, false)
|
||||
s.Require().Nil(err)
|
||||
|
||||
post1, _, err := s.th.App.CreatePost(s.th.Context, &model.Post{Message: model.NewRandomString(15), UserId: s.th.BasicUser.Id, ChannelId: channel.Id}, channel, model.CreatePostFlags{})
|
||||
s.Require().Nil(err)
|
||||
|
||||
post2, _, err := s.th.App.CreatePost(s.th.Context, &model.Post{Message: model.NewRandomString(15), UserId: s.th.BasicUser.Id, ChannelId: channel.Id}, channel, model.CreatePostFlags{})
|
||||
s.Require().Nil(err)
|
||||
|
||||
return channelName, post1, post2
|
||||
}
|
||||
|
||||
s.RunForSystemAdminAndLocal("List all posts for a channel", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
teamName := s.th.BasicTeam.Name
|
||||
channelName, post1, post2 := createNewChannelAndPosts()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("number", 2, "")
|
||||
|
||||
err := postListCmdF(c, cmd, []string{teamName + ":" + channelName})
|
||||
s.Require().Nil(err)
|
||||
s.Equal(2, len(printer.GetLines()))
|
||||
|
||||
printedPost1, ok := printer.GetLines()[0].(*model.Post)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(printedPost1.Message, post1.Message)
|
||||
|
||||
printedPost2, ok := printer.GetLines()[1].(*model.Post)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(printedPost2.Message, post2.Message)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List all posts for a channel without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
teamName := s.th.BasicTeam.Name
|
||||
channelName, _, _ := createNewChannelAndPosts()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("number", 2, "")
|
||||
|
||||
err := postListCmdF(s.th.Client, cmd, []string{teamName + ":" + channelName})
|
||||
s.Require().NotNil(err)
|
||||
//s.Require().Contains(err.Error(), "You do not have the appropriate permissions.")
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("List all posts for a channel with since flag", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
ISO8601ValidString := "2006-01-02T15:04:05-07:00"
|
||||
teamName := s.th.BasicTeam.Name
|
||||
channelName, post1, post2 := createNewChannelAndPosts()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("since", ISO8601ValidString, "")
|
||||
|
||||
err := postListCmdF(c, cmd, []string{teamName + ":" + channelName})
|
||||
s.Require().Nil(err)
|
||||
s.Equal(2, len(printer.GetLines()))
|
||||
|
||||
printedPost1, ok := printer.GetLines()[0].(*model.Post)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(printedPost1.Message, post1.Message)
|
||||
|
||||
printedPost2, ok := printer.GetLines()[1].(*model.Post)
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(printedPost2.Message, post2.Message)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("List all posts for a channel with since flag without permissions", func() {
|
||||
printer.Clean()
|
||||
|
||||
ISO8601ValidString := "2006-01-02T15:04:05-07:00"
|
||||
teamName := s.th.BasicTeam.Name
|
||||
channelName, _, _ := createNewChannelAndPosts()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("since", ISO8601ValidString, "")
|
||||
|
||||
err := postListCmdF(s.th.Client, cmd, []string{teamName + ":" + channelName})
|
||||
s.Require().NotNil(err)
|
||||
//s.Require().Contains(err.Error(), "You do not have the appropriate permissions.")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestPostCreateCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("Create a post for System Admin Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := "some text"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Create a post for Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := "some text"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
err := postCreateCmdF(s.th.Client, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Create a post for Local Client should fail", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := "some text"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
prevLocal := viper.GetBool("local")
|
||||
viper.Set("local", true)
|
||||
defer viper.Set("local", prevLocal)
|
||||
|
||||
err := postCreateCmdF(s.th.LocalClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "creating posts is not supported in local mode")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Send a direct message for System Admin Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := model.NewRandomString(15)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{"@" + s.th.BasicUser2.Username})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
dmChannel, appErr := s.th.App.GetOrCreateDirectChannel(s.th.Context, s.th.SystemAdminUser.Id, s.th.BasicUser2.Id)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
posts, appErr := s.th.App.GetPosts(s.th.Context, dmChannel.Id, 0, 10)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
var matched []*model.Post
|
||||
for _, post := range posts.Posts {
|
||||
if post.Message == msgArg {
|
||||
matched = append(matched, post)
|
||||
}
|
||||
}
|
||||
s.Require().Len(matched, 1, "expected exactly one direct message with the sent text")
|
||||
s.Require().Equal(s.th.SystemAdminUser.Id, matched[0].UserId)
|
||||
s.Require().Equal(dmChannel.Id, matched[0].ChannelId)
|
||||
})
|
||||
|
||||
s.Run("Send a direct message for Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := model.NewRandomString(15)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
err := postCreateCmdF(s.th.Client, cmd, []string{"@" + s.th.BasicUser2.Username})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
dmChannel, appErr := s.th.App.GetOrCreateDirectChannel(s.th.Context, s.th.BasicUser.Id, s.th.BasicUser2.Id)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
posts, appErr := s.th.App.GetPosts(s.th.Context, dmChannel.Id, 0, 10)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
var matched []*model.Post
|
||||
for _, post := range posts.Posts {
|
||||
if post.Message == msgArg {
|
||||
matched = append(matched, post)
|
||||
}
|
||||
}
|
||||
s.Require().Len(matched, 1, "expected exactly one direct message with the sent text")
|
||||
s.Require().Equal(s.th.BasicUser.Id, matched[0].UserId)
|
||||
s.Require().Equal(dmChannel.Id, matched[0].ChannelId)
|
||||
})
|
||||
|
||||
s.Run("Send a direct message for Local Client should fail", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := model.NewRandomString(15)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
prevLocal := viper.GetBool("local")
|
||||
viper.Set("local", true)
|
||||
defer viper.Set("local", prevLocal)
|
||||
|
||||
err := postCreateCmdF(s.th.LocalClient, cmd, []string{"@" + s.th.BasicUser2.Username})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "creating posts is not supported in local mode")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Send a direct message to a non-existing user should fail", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := model.NewRandomString(15)
|
||||
missingUsername := model.NewUsername()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{"@" + missingUsername})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), missingUsername)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Reply to a an existing post for System Admin Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := "some text"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().String("reply-to", s.th.BasicPost.Id, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Reply to a an existing post for Client", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := "some text"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().String("reply-to", s.th.BasicPost.Id, "")
|
||||
|
||||
err := postCreateCmdF(s.th.Client, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Reply to a an existing post for Local Client should fail", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := "some text"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().String("reply-to", s.th.BasicPost.Id, "")
|
||||
prevLocal := viper.GetBool("local")
|
||||
viper.Set("local", true)
|
||||
defer viper.Set("local", prevLocal)
|
||||
|
||||
err := postCreateCmdF(s.th.LocalClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "creating posts is not supported in local mode")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
findPostByMessage := func(channelID, message string) *model.Post {
|
||||
posts, appErr := s.th.App.GetPosts(s.th.Context, channelID, 0, 60)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
var matched []*model.Post
|
||||
for _, post := range posts.Posts {
|
||||
if post.Message == message {
|
||||
matched = append(matched, post)
|
||||
}
|
||||
}
|
||||
s.Require().Len(matched, 1, "expected exactly one post with the sent text")
|
||||
return matched[0]
|
||||
}
|
||||
|
||||
s.Run("Create a post with a file attachment", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := model.NewRandomString(15)
|
||||
fileContent := []byte("mmctl attachment contents")
|
||||
filePath := filepath.Join(s.T().TempDir(), "attachment.txt")
|
||||
s.Require().NoError(os.WriteFile(filePath, fileContent, 0600))
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().StringArray("file", []string{filePath}, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
post := findPostByMessage(s.th.BasicChannel.Id, msgArg)
|
||||
s.Require().Len(post.FileIds, 1)
|
||||
|
||||
infos, _, appErr := s.th.App.GetFileInfosForPost(s.th.Context, post, false, false)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(infos, 1)
|
||||
s.Require().Equal("attachment.txt", infos[0].Name)
|
||||
s.Require().Equal(int64(len(fileContent)), infos[0].Size)
|
||||
})
|
||||
|
||||
s.Run("Create a post with multiple file attachments", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := model.NewRandomString(15)
|
||||
dir := s.T().TempDir()
|
||||
|
||||
firstContent := []byte("first attachment")
|
||||
firstPath := filepath.Join(dir, "first.txt")
|
||||
s.Require().NoError(os.WriteFile(firstPath, firstContent, 0600))
|
||||
|
||||
secondContent := []byte("second attachment")
|
||||
secondPath := filepath.Join(dir, "second.txt")
|
||||
s.Require().NoError(os.WriteFile(secondPath, secondContent, 0600))
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().StringArray("file", []string{firstPath, secondPath}, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
post := findPostByMessage(s.th.BasicChannel.Id, msgArg)
|
||||
s.Require().Len(post.FileIds, 2)
|
||||
|
||||
infos, _, appErr := s.th.App.GetFileInfosForPost(s.th.Context, post, false, false)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(infos, 2)
|
||||
|
||||
names := []string{infos[0].Name, infos[1].Name}
|
||||
s.Require().ElementsMatch([]string{"first.txt", "second.txt"}, names)
|
||||
})
|
||||
|
||||
s.Run("Create a post with only a file attachment and no message", func() {
|
||||
printer.Clean()
|
||||
|
||||
fileContent := []byte("attachment only, no message")
|
||||
filePath := filepath.Join(s.T().TempDir(), "only.txt")
|
||||
s.Require().NoError(os.WriteFile(filePath, fileContent, 0600))
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", "", "")
|
||||
cmd.Flags().StringArray("file", []string{filePath}, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
posts, appErr := s.th.App.GetPosts(s.th.Context, s.th.BasicChannel.Id, 0, 60)
|
||||
s.Require().Nil(appErr)
|
||||
|
||||
var matched []*model.Post
|
||||
for _, post := range posts.Posts {
|
||||
if post.Message == "" && post.Type == "" && len(post.FileIds) == 1 {
|
||||
matched = append(matched, post)
|
||||
}
|
||||
}
|
||||
s.Require().Len(matched, 1, "expected exactly one message-less post with a single file attachment")
|
||||
|
||||
infos, _, appErr := s.th.App.GetFileInfosForPost(s.th.Context, matched[0], false, false)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(infos, 1)
|
||||
s.Require().Equal("only.txt", infos[0].Name)
|
||||
})
|
||||
|
||||
s.Run("Create a post with no message and no file should fail", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", "", "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "a post must have a message or at least one file attachment")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Create a post with a missing file should fail", func() {
|
||||
printer.Clean()
|
||||
|
||||
missingPath := filepath.Join(s.T().TempDir(), "does-not-exist.txt")
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", model.NewRandomString(15), "")
|
||||
cmd.Flags().StringArray("file", []string{missingPath}, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "could not read file")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Create a post with the successful attachments when one upload fails", func() {
|
||||
printer.Clean()
|
||||
|
||||
msgArg := model.NewRandomString(15)
|
||||
dir := s.T().TempDir()
|
||||
|
||||
goodContent := []byte("good attachment")
|
||||
goodPath := filepath.Join(dir, "good.txt")
|
||||
s.Require().NoError(os.WriteFile(goodPath, goodContent, 0600))
|
||||
|
||||
// An empty file is rejected by the server on upload, so it exercises the
|
||||
// mid-batch upload failure path while the readable file still succeeds.
|
||||
emptyPath := filepath.Join(dir, "empty.txt")
|
||||
s.Require().NoError(os.WriteFile(emptyPath, []byte{}, 0600))
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().StringArray("file", []string{goodPath, emptyPath}, "")
|
||||
|
||||
err := postCreateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicTeam.Name + ":" + s.th.BasicChannel.Name})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "could not upload file")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
post := findPostByMessage(s.th.BasicChannel.Id, msgArg)
|
||||
s.Require().Len(post.FileIds, 1)
|
||||
|
||||
infos, _, appErr := s.th.App.GetFileInfosForPost(s.th.Context, post, false, false)
|
||||
s.Require().Nil(appErr)
|
||||
s.Require().Len(infos, 1)
|
||||
s.Require().Equal("good.txt", infos[0].Name)
|
||||
})
|
||||
}
|
||||
927
server/cmd/mmctl/commands/post_test.go
Normal file
927
server/cmd/mmctl/commands/post_test.go
Normal file
@@ -0,0 +1,927 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPostCreateCmdF() {
|
||||
s.Run("create a post with empty text and no files", func() {
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{"some-channel", ""})
|
||||
s.Require().EqualError(err, "a post must have a message or at least one file attachment")
|
||||
})
|
||||
|
||||
s.Run("no channel specified", func() {
|
||||
msgArg := "some text"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{"", msgArg})
|
||||
s.Require().EqualError(err, "Unable to find channel ''")
|
||||
})
|
||||
|
||||
s.Run("wrong reply msg", func() {
|
||||
msgArg := "some text"
|
||||
replyToArg := "a-non-existing-post"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().String("reply-to", replyToArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPost(context.TODO(), replyToArg, "").
|
||||
Return(nil, &model.Response{}, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{msgArg})
|
||||
s.Require().Contains(err.Error(), "some-error")
|
||||
})
|
||||
|
||||
s.Run("error when creating a post", func() {
|
||||
msgArg := "some text"
|
||||
channelArg := "example-channel"
|
||||
mockChannel := model.Channel{Name: channelArg}
|
||||
mockPost := &model.Post{Message: msgArg}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{channelArg, msgArg})
|
||||
s.Require().Contains(err.Error(), "could not create post")
|
||||
})
|
||||
|
||||
s.Run("create a post", func() {
|
||||
msgArg := "some text"
|
||||
channelArg := "example-channel"
|
||||
mockChannel := model.Channel{Name: channelArg}
|
||||
mockPost := model.Post{Message: msgArg}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{channelArg, msgArg})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("create a post in local mode should fail", func() {
|
||||
msgArg := "some text"
|
||||
channelArg := "example-channel"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
prevLocal := viper.GetBool("local")
|
||||
viper.Set("local", true)
|
||||
defer viper.Set("local", prevLocal)
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().EqualError(err, "creating posts is not supported in local mode")
|
||||
})
|
||||
|
||||
s.Run("create a direct message", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
username := "target-user"
|
||||
meID := "my-user-id"
|
||||
targetUserID := "target-user-id"
|
||||
dmChannelID := "dm-channel-id"
|
||||
|
||||
mockTargetUser := model.User{Id: targetUserID, Username: username}
|
||||
mockMe := model.User{Id: meID}
|
||||
mockChannel := model.Channel{Id: dmChannelID, Type: model.ChannelTypeDirect}
|
||||
mockPost := model.Post{Message: msgArg, ChannelId: dmChannelID}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), username, "").
|
||||
Return(&mockTargetUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMe(context.TODO(), "").
|
||||
Return(&mockMe, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateDirectChannel(context.TODO(), meID, targetUserID).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{"@" + username})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("create a direct message with reply-to and burn-on-read", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
username := "target-user"
|
||||
meID := "my-user-id"
|
||||
targetUserID := "target-user-id"
|
||||
dmChannelID := "dm-channel-id"
|
||||
replyToArg := "reply-to-post"
|
||||
rootID := "root-post-id"
|
||||
|
||||
mockTargetUser := model.User{Id: targetUserID, Username: username}
|
||||
mockMe := model.User{Id: meID}
|
||||
mockChannel := model.Channel{Id: dmChannelID, Type: model.ChannelTypeDirect}
|
||||
mockReplyTo := model.Post{RootId: rootID}
|
||||
mockPost := model.Post{Message: msgArg, ChannelId: dmChannelID, RootId: rootID, Type: model.PostTypeBurnOnRead}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().String("reply-to", replyToArg, "")
|
||||
cmd.Flags().Bool("burn-on-read", true, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPost(context.TODO(), replyToArg, "").
|
||||
Return(&mockReplyTo, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), username, "").
|
||||
Return(&mockTargetUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMe(context.TODO(), "").
|
||||
Return(&mockMe, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateDirectChannel(context.TODO(), meID, targetUserID).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{"@" + username})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("direct message to a non-existing user", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
username := "ghost"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), username, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), username, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{"@" + username})
|
||||
var nfErr ErrEntityNotFound
|
||||
s.Require().ErrorAs(err, &nfErr)
|
||||
s.Require().Equal("user", nfErr.Type)
|
||||
s.Require().Equal(username, nfErr.ID)
|
||||
})
|
||||
|
||||
s.Run("direct message fails to resolve the current user", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
username := "target-user"
|
||||
targetUserID := "target-user-id"
|
||||
mockTargetUser := model.User{Id: targetUserID, Username: username}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), username, "").
|
||||
Return(&mockTargetUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMe(context.TODO(), "").
|
||||
Return(nil, &model.Response{}, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{"@" + username})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "could not retrieve the current user")
|
||||
})
|
||||
|
||||
s.Run("direct message fails to create the direct channel", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
username := "target-user"
|
||||
meID := "my-user-id"
|
||||
targetUserID := "target-user-id"
|
||||
mockTargetUser := model.User{Id: targetUserID, Username: username}
|
||||
mockMe := model.User{Id: meID}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), username, "").
|
||||
Return(&mockTargetUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetMe(context.TODO(), "").
|
||||
Return(&mockMe, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateDirectChannel(context.TODO(), meID, targetUserID).
|
||||
Return(nil, &model.Response{}, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{"@" + username})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "could not create direct channel with 'target-user'")
|
||||
})
|
||||
|
||||
s.Run("reply to an existing post", func() {
|
||||
msgArg := "some text"
|
||||
replyToArg := "an-existing-post"
|
||||
rootID := "some-root-id"
|
||||
channelArg := "example-channel"
|
||||
mockChannel := model.Channel{Name: channelArg}
|
||||
mockReplyTo := model.Post{RootId: rootID}
|
||||
mockPost := model.Post{Message: msgArg, RootId: rootID}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("reply-to", replyToArg, "")
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPost(context.TODO(), replyToArg, "").
|
||||
Return(&mockReplyTo, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{channelArg, msgArg})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("create a post with a file attachment", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
channelArg := "example-channel"
|
||||
channelID := "channel-id"
|
||||
fileID := "file-id-1"
|
||||
fileContent := []byte("file contents")
|
||||
filePath := filepath.Join(s.T().TempDir(), "attachment.txt")
|
||||
s.Require().NoError(os.WriteFile(filePath, fileContent, 0600))
|
||||
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelArg}
|
||||
mockPost := model.Post{Message: msgArg, ChannelId: channelID, FileIds: []string{fileID}}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().StringArray("file", []string{filePath}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUpload(context.TODO(), &model.UploadSession{Type: model.UploadTypeAttachment, ChannelId: channelID, Filename: "attachment.txt", FileSize: int64(len(fileContent)), UserId: "me"}).
|
||||
Return(&model.UploadSession{Id: "upload-1"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadData(context.TODO(), "upload-1", gomock.Any()).
|
||||
Return(&model.FileInfo{Id: fileID}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{channelArg, msgArg})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("create a post with multiple file attachments", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
channelArg := "example-channel"
|
||||
channelID := "channel-id"
|
||||
dir := s.T().TempDir()
|
||||
|
||||
firstContent := []byte("first file")
|
||||
firstPath := filepath.Join(dir, "first.txt")
|
||||
s.Require().NoError(os.WriteFile(firstPath, firstContent, 0600))
|
||||
|
||||
secondContent := []byte("second file")
|
||||
secondPath := filepath.Join(dir, "second.png")
|
||||
s.Require().NoError(os.WriteFile(secondPath, secondContent, 0600))
|
||||
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelArg}
|
||||
mockPost := model.Post{Message: msgArg, ChannelId: channelID, FileIds: []string{"file-id-1", "file-id-2"}}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().StringArray("file", []string{firstPath, secondPath}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUpload(context.TODO(), &model.UploadSession{Type: model.UploadTypeAttachment, ChannelId: channelID, Filename: "first.txt", FileSize: int64(len(firstContent)), UserId: "me"}).
|
||||
Return(&model.UploadSession{Id: "upload-1"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadData(context.TODO(), "upload-1", gomock.Any()).
|
||||
Return(&model.FileInfo{Id: "file-id-1"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUpload(context.TODO(), &model.UploadSession{Type: model.UploadTypeAttachment, ChannelId: channelID, Filename: "second.png", FileSize: int64(len(secondContent)), UserId: "me"}).
|
||||
Return(&model.UploadSession{Id: "upload-2"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadData(context.TODO(), "upload-2", gomock.Any()).
|
||||
Return(&model.FileInfo{Id: "file-id-2"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{channelArg, msgArg})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("create a post with only a file attachment and no message", func() {
|
||||
printer.Clean()
|
||||
channelArg := "example-channel"
|
||||
channelID := "channel-id"
|
||||
fileID := "file-id-1"
|
||||
fileContent := []byte("file contents")
|
||||
filePath := filepath.Join(s.T().TempDir(), "attachment.txt")
|
||||
s.Require().NoError(os.WriteFile(filePath, fileContent, 0600))
|
||||
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelArg}
|
||||
mockPost := model.Post{ChannelId: channelID, FileIds: []string{fileID}}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", "", "")
|
||||
cmd.Flags().StringArray("file", []string{filePath}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUpload(context.TODO(), &model.UploadSession{Type: model.UploadTypeAttachment, ChannelId: channelID, Filename: "attachment.txt", FileSize: int64(len(fileContent)), UserId: "me"}).
|
||||
Return(&model.UploadSession{Id: "upload-1"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadData(context.TODO(), "upload-1", gomock.Any()).
|
||||
Return(&model.FileInfo{Id: fileID}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("fails when an upload does not complete and message is empty", func() {
|
||||
printer.Clean()
|
||||
channelArg := "example-channel"
|
||||
channelID := "channel-id"
|
||||
fileContent := []byte("file contents")
|
||||
filePath := filepath.Join(s.T().TempDir(), "attachment.txt")
|
||||
s.Require().NoError(os.WriteFile(filePath, fileContent, 0600))
|
||||
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelArg}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", "", "")
|
||||
cmd.Flags().StringArray("file", []string{filePath}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUpload(context.TODO(), &model.UploadSession{Type: model.UploadTypeAttachment, ChannelId: channelID, Filename: "attachment.txt", FileSize: int64(len(fileContent)), UserId: "me"}).
|
||||
Return(&model.UploadSession{Id: "upload-1"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadData(context.TODO(), "upload-1", gomock.Any()).
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{channelArg})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "upload did not complete")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("creates a post with the successful attachments when a later upload errors", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
channelArg := "example-channel"
|
||||
channelID := "channel-id"
|
||||
dir := s.T().TempDir()
|
||||
|
||||
firstContent := []byte("first file")
|
||||
firstPath := filepath.Join(dir, "first.txt")
|
||||
s.Require().NoError(os.WriteFile(firstPath, firstContent, 0600))
|
||||
|
||||
secondContent := []byte("second file")
|
||||
secondPath := filepath.Join(dir, "second.txt")
|
||||
s.Require().NoError(os.WriteFile(secondPath, secondContent, 0600))
|
||||
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelArg}
|
||||
mockPost := model.Post{Message: msgArg, ChannelId: channelID, FileIds: []string{"file-id-1"}}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().StringArray("file", []string{firstPath, secondPath}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUpload(context.TODO(), &model.UploadSession{Type: model.UploadTypeAttachment, ChannelId: channelID, Filename: "first.txt", FileSize: int64(len(firstContent)), UserId: "me"}).
|
||||
Return(&model.UploadSession{Id: "upload-1"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadData(context.TODO(), "upload-1", gomock.Any()).
|
||||
Return(&model.FileInfo{Id: "file-id-1"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUpload(context.TODO(), &model.UploadSession{Type: model.UploadTypeAttachment, ChannelId: channelID, Filename: "second.txt", FileSize: int64(len(secondContent)), UserId: "me"}).
|
||||
Return(&model.UploadSession{Id: "upload-2"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadData(context.TODO(), "upload-2", gomock.Any()).
|
||||
Return(nil, &model.Response{}, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{channelArg, msgArg})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "could not upload file")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("fails when the file cannot be read", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
channelArg := "example-channel"
|
||||
channelID := "channel-id"
|
||||
missingPath := filepath.Join(s.T().TempDir(), "does-not-exist.txt")
|
||||
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelArg}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().StringArray("file", []string{missingPath}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := postCreateCmdF(s.client, cmd, []string{channelArg, msgArg})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "could not read file")
|
||||
})
|
||||
|
||||
s.Run("creates a post with the message when the only upload errors", func() {
|
||||
printer.Clean()
|
||||
msgArg := "some text"
|
||||
channelArg := "example-channel"
|
||||
channelID := "channel-id"
|
||||
fileContent := []byte("file contents")
|
||||
filePath := filepath.Join(s.T().TempDir(), "attachment.txt")
|
||||
s.Require().NoError(os.WriteFile(filePath, fileContent, 0600))
|
||||
|
||||
mockChannel := model.Channel{Id: channelID, Name: channelArg}
|
||||
mockPost := model.Post{Message: msgArg, ChannelId: channelID}
|
||||
data, err := mockPost.ToJSON()
|
||||
s.Require().NoError(err)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("message", msgArg, "")
|
||||
cmd.Flags().StringArray("file", []string{filePath}, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelArg).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
CreateUpload(context.TODO(), &model.UploadSession{Type: model.UploadTypeAttachment, ChannelId: channelID, Filename: "attachment.txt", FileSize: int64(len(fileContent)), UserId: "me"}).
|
||||
Return(&model.UploadSession{Id: "upload-1"}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UploadData(context.TODO(), "upload-1", gomock.Any()).
|
||||
Return(nil, &model.Response{}, errors.New("some-error")).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
DoAPIPost(context.TODO(), "/posts?set_online=false", data).
|
||||
Return(nil, nil).
|
||||
Times(1)
|
||||
|
||||
err = postCreateCmdF(s.client, cmd, []string{channelArg, msgArg})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "could not upload file")
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestPostListCmdF() {
|
||||
s.Run("no channel specified", func() {
|
||||
sinceArg := "invalid-date"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("since", sinceArg, "")
|
||||
|
||||
err := postListCmdF(s.client, cmd, []string{"", sinceArg})
|
||||
s.Require().EqualError(err, "Unable to find channel ''")
|
||||
})
|
||||
|
||||
s.Run("invalid time for since flag", func() {
|
||||
sinceArg := "invalid-date"
|
||||
mockChannel := model.Channel{Name: channelName}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("since", sinceArg, "")
|
||||
|
||||
err := postListCmdF(s.client, cmd, []string{channelName, sinceArg})
|
||||
s.Require().Contains(err.Error(), "invalid since time 'invalid-date'")
|
||||
})
|
||||
|
||||
s.Run("list posts for a channel", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
mockPost := &model.Post{Message: "some text", Id: "some-id", UserId: userID, CreateAt: model.GetMillisForTime(time.Now())}
|
||||
mockPostList := model.NewPostList()
|
||||
mockPostList.AddPost(mockPost)
|
||||
mockPostList.AddOrder(mockPost.Id)
|
||||
mockUser := model.User{Id: userID, Username: "some-user"}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("number", 1, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPostsForChannel(context.TODO(), channelID, 0, 1, "", false, false).
|
||||
Return(mockPostList, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), userID, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
printer.Clean()
|
||||
err := postListCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], mockPost)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("list posts for a channel from a certain time (valid date)", func() {
|
||||
printer.Clean()
|
||||
|
||||
ISO8601ValidString := "2006-01-02T15:04:05-07:00"
|
||||
|
||||
sinceArg := "2006-01-02T15:04:05-07:00"
|
||||
sinceTime, err := time.Parse(ISO8601ValidString, sinceArg)
|
||||
s.Require().Nil(err)
|
||||
|
||||
sinceTimeMillis := model.GetMillisForTime(sinceTime)
|
||||
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
mockPost := &model.Post{Message: "some text", Id: "some-id", UserId: userID}
|
||||
mockPostList := model.NewPostList()
|
||||
mockPostList.AddPost(mockPost)
|
||||
mockPostList.AddOrder(mockPost.Id)
|
||||
mockUser := model.User{Id: userID, Username: "some-user"}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("number", 1, "")
|
||||
cmd.Flags().String("since", sinceArg, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPostsSince(context.TODO(), channelID, sinceTimeMillis, false).
|
||||
Return(mockPostList, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), userID, "").
|
||||
Return(&mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err = postListCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(printer.GetLines()[0], mockPost)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestDeletePostsCmdF() {
|
||||
postID1 := "ux9bxc1b8bf1zdoj1tfu14836e"
|
||||
postID2 := "ux9bxc1b8bf1zdoj1tfu14836f"
|
||||
|
||||
s.Run("invalid post id", func() {
|
||||
id := "invalid-id"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
cmd.Flags().Bool("permanent", false, "")
|
||||
|
||||
err := deletePostsCmdF(s.client, cmd, []string{id})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal("Invalid postID: invalid-id", printer.GetErrorLines()[0])
|
||||
})
|
||||
|
||||
s.Run("successfully permanently delete one post", func() {
|
||||
printer.Clean()
|
||||
s.client.
|
||||
EXPECT().
|
||||
PermanentDeletePost(context.TODO(), postID1).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
cmd.Flags().Bool("permanent", true, "")
|
||||
|
||||
err := deletePostsCmdF(s.client, cmd, []string{postID1})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(postID1+" successfully deleted", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("successfully soft delete one post", func() {
|
||||
printer.Clean()
|
||||
s.client.
|
||||
EXPECT().
|
||||
DeletePost(context.TODO(), postID1).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
cmd.Flags().Bool("permanent", false, "")
|
||||
|
||||
err := deletePostsCmdF(s.client, cmd, []string{postID1})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(postID1+" successfully deleted", printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("successfully delete multiple posts", func() {
|
||||
printer.Clean()
|
||||
s.client.
|
||||
EXPECT().
|
||||
PermanentDeletePost(context.TODO(), postID1).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
PermanentDeletePost(context.TODO(), postID2).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
cmd.Flags().Bool("permanent", true, "")
|
||||
|
||||
err := deletePostsCmdF(s.client, cmd, []string{postID1, postID2})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Equal(postID1+" successfully deleted", printer.GetLines()[0])
|
||||
s.Require().Equal(postID2+" successfully deleted", printer.GetLines()[1])
|
||||
})
|
||||
|
||||
s.Run("PermanentDeletePost api request returns an error", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockError := errors.New("an error occurred on deleting a post")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
PermanentDeletePost(context.TODO(), postID1).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockError).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
cmd.Flags().Bool("permanent", true, "")
|
||||
|
||||
err := deletePostsCmdF(s.client, cmd, []string{postID1})
|
||||
s.Require().ErrorContains(err, "an error occurred on deleting a post")
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal("Error deleting post: "+postID1+". Error: an error occurred on deleting a post",
|
||||
printer.GetErrorLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Delete multiple posts but one fails with an error", func() {
|
||||
printer.Clean()
|
||||
mockError := errors.New("an error occurred on deleting a post")
|
||||
s.client.
|
||||
EXPECT().
|
||||
PermanentDeletePost(context.TODO(), postID1).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
s.client.
|
||||
EXPECT().
|
||||
PermanentDeletePost(context.TODO(), postID2).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockError).
|
||||
Times(1)
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("confirm", true, "")
|
||||
cmd.Flags().Bool("permanent", true, "")
|
||||
|
||||
err := deletePostsCmdF(s.client, cmd, []string{postID1, postID2})
|
||||
s.Require().ErrorContains(err, "an error occurred on deleting a post")
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(postID1+" successfully deleted", printer.GetLines()[0])
|
||||
s.Require().Equal("Error deleting post: "+postID2+". Error: an error occurred on deleting a post",
|
||||
printer.GetErrorLines()[0])
|
||||
})
|
||||
}
|
||||
159
server/cmd/mmctl/commands/report.go
Normal file
159
server/cmd/mmctl/commands/report.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ReportCmd = &cobra.Command{
|
||||
Use: "report",
|
||||
Short: "Reporting commands",
|
||||
}
|
||||
|
||||
var ReportPostsCmd = &cobra.Command{
|
||||
Use: "posts [channel]",
|
||||
Short: "Retrieve posts for reporting purposes",
|
||||
Long: `Retrieve posts from a channel for reporting purposes. This command supports
|
||||
pagination and can filter posts by time range. Results can be output in JSON format
|
||||
for further processing.`,
|
||||
Example: ` # Get posts from a channel with default settings
|
||||
mmctl report posts myteam:mychannel
|
||||
|
||||
# Get posts with JSON output
|
||||
mmctl report posts myteam:mychannel --json
|
||||
|
||||
# Get posts sorted by update_at in descending order
|
||||
mmctl report posts myteam:mychannel --time-field update_at --sort-direction desc
|
||||
|
||||
# Get posts including deleted posts and metadata
|
||||
mmctl report posts myteam:mychannel --include-deleted --include-metadata
|
||||
|
||||
# Get posts excluding ALL system posts
|
||||
mmctl report posts myteam:mychannel --exclude-system-posts
|
||||
|
||||
# Get more posts per page (max 1000)
|
||||
mmctl report posts myteam:mychannel --per-page 500
|
||||
|
||||
# Resume pagination from a specific cursor (use next_cursor from previous response)
|
||||
mmctl report posts myteam:mychannel --cursor "MTphYmMxMjM6Y3JlYXRlX2F0OmZhbHNlOmZhbHNlOmFzYzoxNjQwMDAwMzAwMDAwOnBvc3Qz"`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(reportPostsCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ReportPostsCmd.Flags().String("time-field", "create_at", "Time field to use for sorting (create_at or update_at)")
|
||||
ReportPostsCmd.Flags().String("sort-direction", "asc", "Sort direction (asc or desc)")
|
||||
ReportPostsCmd.Flags().String("cursor", "", "Opaque cursor for pagination (use next_cursor from previous response)")
|
||||
ReportPostsCmd.Flags().Int("per-page", 100, "Number of posts per page (max 1000)")
|
||||
ReportPostsCmd.Flags().Bool("include-deleted", false, "Include deleted posts")
|
||||
ReportPostsCmd.Flags().Bool("exclude-system-posts", false, "Exclude ALL system posts (any type starting with 'system_')")
|
||||
ReportPostsCmd.Flags().Bool("include-metadata", false, "Include file info, reactions, etc.")
|
||||
|
||||
ReportCmd.AddCommand(ReportPostsCmd)
|
||||
RootCmd.AddCommand(ReportCmd)
|
||||
}
|
||||
|
||||
func reportPostsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
channel := getChannelFromChannelArg(c, args[0])
|
||||
if channel == nil {
|
||||
return errors.New("Unable to find channel '" + args[0] + "'")
|
||||
}
|
||||
|
||||
// Get flags
|
||||
timeField, _ := cmd.Flags().GetString("time-field")
|
||||
sortDirection, _ := cmd.Flags().GetString("sort-direction")
|
||||
cursor, _ := cmd.Flags().GetString("cursor")
|
||||
perPage, _ := cmd.Flags().GetInt("per-page")
|
||||
includeDeleted, _ := cmd.Flags().GetBool("include-deleted")
|
||||
excludeSystemPosts, _ := cmd.Flags().GetBool("exclude-system-posts")
|
||||
includeMetadata, _ := cmd.Flags().GetBool("include-metadata")
|
||||
|
||||
// Validate flags (only validated for first request; ignored when cursor is provided)
|
||||
if timeField != model.ReportingTimeFieldCreateAt && timeField != model.ReportingTimeFieldUpdateAt {
|
||||
return errors.New("time-field must be either 'create_at' or 'update_at'")
|
||||
}
|
||||
if sortDirection != model.ReportingSortDirectionAsc && sortDirection != model.ReportingSortDirectionDesc {
|
||||
return errors.New("sort-direction must be either 'asc' or 'desc'")
|
||||
}
|
||||
if perPage <= 0 || perPage > model.MaxReportingPerPage {
|
||||
return fmt.Errorf("per-page must be between 1 and %d", model.MaxReportingPerPage)
|
||||
}
|
||||
|
||||
// Set up options
|
||||
options := model.ReportPostOptions{
|
||||
ChannelId: channel.Id,
|
||||
TimeField: timeField,
|
||||
SortDirection: sortDirection,
|
||||
PerPage: perPage,
|
||||
IncludeDeleted: includeDeleted,
|
||||
ExcludeSystemPosts: excludeSystemPosts,
|
||||
IncludeMetadata: includeMetadata,
|
||||
}
|
||||
|
||||
// Initialize cursor (opaque string)
|
||||
cursorObj := model.ReportPostOptionsCursor{
|
||||
Cursor: cursor,
|
||||
}
|
||||
|
||||
response, _, err := c.GetPostsForReporting(context.TODO(), options, cursorObj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get posts for reporting: %w", err)
|
||||
}
|
||||
|
||||
// Print posts (already in correct order from API)
|
||||
for _, post := range response.Posts {
|
||||
printReportPost(post)
|
||||
}
|
||||
|
||||
// Show pagination info
|
||||
printer.Print(fmt.Sprintf("\nShowing %d posts.", len(response.Posts)))
|
||||
if response.NextCursor != nil && response.NextCursor.Cursor != "" {
|
||||
printer.Print(fmt.Sprintf("To get the next page, use: --cursor \"%s\"", response.NextCursor.Cursor))
|
||||
} else {
|
||||
printer.Print("No more posts available.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printReportPost(post *model.Post) {
|
||||
jsonOutput, _ := RootCmd.Flags().GetBool("json")
|
||||
|
||||
if jsonOutput {
|
||||
jsonBytes, err := json.MarshalIndent(post, "", " ")
|
||||
if err != nil {
|
||||
printer.PrintError("Error marshaling post to JSON: " + err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
} else {
|
||||
// Print in a readable format
|
||||
createdAt := time.Unix(post.CreateAt/1000, 0).Format("2006-01-02 15:04:05")
|
||||
updatedAt := time.Unix(post.UpdateAt/1000, 0).Format("2006-01-02 15:04:05")
|
||||
|
||||
fmt.Fprintf(os.Stdout, "Post ID: %s\n", post.Id)
|
||||
fmt.Fprintf(os.Stdout, "User ID: %s\n", post.UserId)
|
||||
fmt.Fprintf(os.Stdout, "Channel ID: %s\n", post.ChannelId)
|
||||
fmt.Fprintf(os.Stdout, "Created At: %s\n", createdAt)
|
||||
fmt.Fprintf(os.Stdout, "Updated At: %s\n", updatedAt)
|
||||
fmt.Fprintf(os.Stdout, "Message: %s\n", printer.SanitizeForTerminal(post.Message))
|
||||
if post.DeleteAt > 0 {
|
||||
deletedAt := time.Unix(post.DeleteAt/1000, 0).Format("2006-01-02 15:04:05")
|
||||
fmt.Fprintf(os.Stdout, "Deleted At: %s\n", deletedAt)
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "---\n")
|
||||
}
|
||||
}
|
||||
364
server/cmd/mmctl/commands/report_test.go
Normal file
364
server/cmd/mmctl/commands/report_test.go
Normal file
@@ -0,0 +1,364 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build unit
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestReportPostsCmdF() {
|
||||
s.Run("no channel specified", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "create_at", "")
|
||||
cmd.Flags().String("sort-direction", "asc", "")
|
||||
cmd.Flags().String("cursor", "", "")
|
||||
cmd.Flags().Int("per-page", 100, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{""})
|
||||
s.Require().EqualError(err, "Unable to find channel ''")
|
||||
})
|
||||
|
||||
s.Run("invalid time-field", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "invalid_field", "")
|
||||
cmd.Flags().String("sort-direction", "asc", "")
|
||||
cmd.Flags().String("cursor", "", "")
|
||||
cmd.Flags().Int("per-page", 100, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().EqualError(err, "time-field must be either 'create_at' or 'update_at'")
|
||||
})
|
||||
|
||||
s.Run("invalid sort-direction", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "create_at", "")
|
||||
cmd.Flags().String("sort-direction", "invalid", "")
|
||||
cmd.Flags().String("cursor", "", "")
|
||||
cmd.Flags().Int("per-page", 100, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().EqualError(err, "sort-direction must be either 'asc' or 'desc'")
|
||||
})
|
||||
|
||||
s.Run("invalid per-page (too large)", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "create_at", "")
|
||||
cmd.Flags().String("sort-direction", "asc", "")
|
||||
cmd.Flags().String("cursor", "", "")
|
||||
cmd.Flags().Int("per-page", 2000, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Contains(err.Error(), "per-page must be between 1 and")
|
||||
})
|
||||
|
||||
s.Run("invalid per-page (zero)", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "create_at", "")
|
||||
cmd.Flags().String("sort-direction", "asc", "")
|
||||
cmd.Flags().String("cursor", "", "")
|
||||
cmd.Flags().Int("per-page", 0, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Contains(err.Error(), "per-page must be between 1 and")
|
||||
})
|
||||
|
||||
s.Run("API error when getting posts", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
mockError := errors.New("api error occurred")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPostsForReporting(context.TODO(), model.ReportPostOptions{
|
||||
ChannelId: channelID,
|
||||
TimeField: "create_at",
|
||||
SortDirection: "asc",
|
||||
PerPage: 100,
|
||||
IncludeDeleted: false,
|
||||
ExcludeSystemPosts: false,
|
||||
IncludeMetadata: false,
|
||||
}, model.ReportPostOptionsCursor{
|
||||
Cursor: "",
|
||||
}).
|
||||
Return(nil, &model.Response{}, mockError).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "create_at", "")
|
||||
cmd.Flags().String("sort-direction", "asc", "")
|
||||
cmd.Flags().String("cursor", "", "")
|
||||
cmd.Flags().Int("per-page", 100, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Contains(err.Error(), "failed to get posts for reporting")
|
||||
})
|
||||
|
||||
s.Run("successfully get posts with no next cursor", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
mockPost1 := &model.Post{Id: "post1", Message: "message1", UserId: userID, ChannelId: channelID}
|
||||
mockPost2 := &model.Post{Id: "post2", Message: "message2", UserId: userID, ChannelId: channelID}
|
||||
mockResponse := &model.ReportPostListResponse{
|
||||
Posts: []*model.Post{
|
||||
mockPost1,
|
||||
mockPost2,
|
||||
},
|
||||
NextCursor: nil,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPostsForReporting(context.TODO(), model.ReportPostOptions{
|
||||
ChannelId: channelID,
|
||||
TimeField: "create_at",
|
||||
SortDirection: "asc",
|
||||
PerPage: 100,
|
||||
IncludeDeleted: false,
|
||||
ExcludeSystemPosts: false,
|
||||
IncludeMetadata: false,
|
||||
}, model.ReportPostOptionsCursor{
|
||||
Cursor: "",
|
||||
}).
|
||||
Return(mockResponse, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "create_at", "")
|
||||
cmd.Flags().String("sort-direction", "asc", "")
|
||||
cmd.Flags().String("cursor", "", "")
|
||||
cmd.Flags().Int("per-page", 100, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("successfully get posts with next cursor", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
mockPost1 := &model.Post{Id: "post1", Message: "message1", UserId: userID, ChannelId: channelID}
|
||||
nextCursor := &model.ReportPostOptionsCursor{
|
||||
Cursor: "MTphYmMxMjM6Y3JlYXRlX2F0OmZhbHNlOmZhbHNlOmFzYzoxNzM1NDg4MTIzNDU2OnBvc3Qx",
|
||||
}
|
||||
mockResponse := &model.ReportPostListResponse{
|
||||
Posts: []*model.Post{
|
||||
mockPost1,
|
||||
},
|
||||
NextCursor: nextCursor,
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPostsForReporting(context.TODO(), model.ReportPostOptions{
|
||||
ChannelId: channelID,
|
||||
TimeField: "create_at",
|
||||
SortDirection: "asc",
|
||||
PerPage: 100,
|
||||
IncludeDeleted: false,
|
||||
ExcludeSystemPosts: false,
|
||||
IncludeMetadata: false,
|
||||
}, model.ReportPostOptionsCursor{
|
||||
Cursor: "",
|
||||
}).
|
||||
Return(mockResponse, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "create_at", "")
|
||||
cmd.Flags().String("sort-direction", "asc", "")
|
||||
cmd.Flags().String("cursor", "", "")
|
||||
cmd.Flags().Int("per-page", 100, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("successfully get posts with custom cursor", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
mockPost1 := &model.Post{Id: "post1", Message: "message1", UserId: userID, ChannelId: channelID}
|
||||
mockResponse := &model.ReportPostListResponse{
|
||||
Posts: []*model.Post{
|
||||
mockPost1,
|
||||
},
|
||||
NextCursor: nil,
|
||||
}
|
||||
|
||||
customCursor := "MTphYmMxMjM6Y3JlYXRlX2F0OmZhbHNlOmZhbHNlOmFzYzoxNzM1NDg4MDAwMDAwOmN1c3RvbXBvc3QxMjM"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPostsForReporting(context.TODO(), model.ReportPostOptions{
|
||||
ChannelId: channelID,
|
||||
TimeField: "create_at",
|
||||
SortDirection: "asc",
|
||||
PerPage: 100,
|
||||
IncludeDeleted: false,
|
||||
ExcludeSystemPosts: false,
|
||||
IncludeMetadata: false,
|
||||
}, model.ReportPostOptionsCursor{
|
||||
Cursor: customCursor,
|
||||
}).
|
||||
Return(mockResponse, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "create_at", "")
|
||||
cmd.Flags().String("sort-direction", "asc", "")
|
||||
cmd.Flags().Int64("end-time", int64(0), "")
|
||||
cmd.Flags().String("cursor", customCursor, "")
|
||||
cmd.Flags().Int("per-page", 100, "")
|
||||
cmd.Flags().Bool("include-deleted", false, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", false, "")
|
||||
cmd.Flags().Bool("include-metadata", false, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("successfully get posts with all options enabled", func() {
|
||||
printer.Clean()
|
||||
mockChannel := model.Channel{Name: channelName, Id: channelID}
|
||||
mockPost1 := &model.Post{Id: "post1", Message: "message1", UserId: userID, ChannelId: channelID}
|
||||
mockResponse := &model.ReportPostListResponse{
|
||||
Posts: []*model.Post{
|
||||
mockPost1,
|
||||
},
|
||||
NextCursor: nil,
|
||||
}
|
||||
|
||||
customCursor := "MTphYmMxMjM6dXBkYXRlX2F0OnRydWU6dHJ1ZTpkZXNjOjE3MzU0MDAwMDAwMDA6"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetChannel(context.TODO(), channelName).
|
||||
Return(&mockChannel, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetPostsForReporting(context.TODO(), model.ReportPostOptions{
|
||||
ChannelId: channelID,
|
||||
TimeField: "update_at",
|
||||
SortDirection: "desc",
|
||||
PerPage: 500,
|
||||
IncludeDeleted: true,
|
||||
ExcludeSystemPosts: true,
|
||||
IncludeMetadata: true,
|
||||
}, model.ReportPostOptionsCursor{
|
||||
Cursor: customCursor,
|
||||
}).
|
||||
Return(mockResponse, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("time-field", "update_at", "")
|
||||
cmd.Flags().String("sort-direction", "desc", "")
|
||||
cmd.Flags().String("cursor", customCursor, "")
|
||||
cmd.Flags().Int("per-page", 500, "")
|
||||
cmd.Flags().Bool("include-deleted", true, "")
|
||||
cmd.Flags().Bool("exclude-system-posts", true, "")
|
||||
cmd.Flags().Bool("include-metadata", true, "")
|
||||
|
||||
err := reportPostsCmdF(s.client, cmd, []string{channelName})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
155
server/cmd/mmctl/commands/roles.go
Normal file
155
server/cmd/mmctl/commands/roles.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var RolesCmd = &cobra.Command{
|
||||
Use: "roles",
|
||||
Short: "Manage user roles",
|
||||
}
|
||||
|
||||
var RolesSystemAdminCmd = &cobra.Command{
|
||||
Use: "system-admin [users]",
|
||||
Aliases: []string{"system_admin"},
|
||||
Short: "Set a user as system admin",
|
||||
Long: "Make some users system admins.",
|
||||
Example: ` # You can make one user a sysadmin
|
||||
$ mmctl roles system-admin john_doe
|
||||
|
||||
# Or promote multiple users at the same time
|
||||
$ mmctl roles system-admin john_doe jane_doe`,
|
||||
RunE: withClient(rolesSystemAdminCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
|
||||
var RolesMemberCmd = &cobra.Command{
|
||||
Use: "member [users]",
|
||||
Short: "Remove system admin privileges",
|
||||
Long: "Remove system admin privileges from some users.",
|
||||
Example: ` # You can remove admin privileges from one user
|
||||
$ mmctl roles member john_doe
|
||||
|
||||
# Or demote multiple users at the same time
|
||||
$ mmctl roles member john_doe jane_doe`,
|
||||
RunE: withClient(rolesMemberCmdF),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
}
|
||||
var RolesListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List available roles",
|
||||
Example: ` $ mmctl roles list
|
||||
$ mmctl roles list --json`,
|
||||
RunE: withClient(rolesListCmdF),
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RolesCmd.AddCommand(
|
||||
RolesSystemAdminCmd,
|
||||
RolesMemberCmd,
|
||||
RolesListCmd,
|
||||
)
|
||||
|
||||
RootCmd.AddCommand(RolesCmd)
|
||||
}
|
||||
|
||||
func rolesListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
roles, _, err := c.GetAllRoles(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get roles: %w", err)
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
printer.PrintT("{{.Name}}", role)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rolesSystemAdminCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
||||
var errs *multierror.Error
|
||||
users := getUsersFromUserArgs(c, args)
|
||||
for i, user := range users {
|
||||
if user == nil {
|
||||
userErr := fmt.Errorf("unable to find user %q", args[i])
|
||||
errs = multierror.Append(errs, userErr)
|
||||
printer.PrintError(userErr.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
systemAdmin := false
|
||||
roles := strings.Fields(user.Roles)
|
||||
for _, role := range roles {
|
||||
if role == model.SystemAdminRoleId {
|
||||
systemAdmin = true
|
||||
}
|
||||
}
|
||||
|
||||
if !systemAdmin {
|
||||
roles = append(roles, model.SystemAdminRoleId)
|
||||
if _, err := c.UpdateUserRoles(context.TODO(), user.Id, strings.Join(roles, " ")); err != nil {
|
||||
updateErr := fmt.Errorf("can't update roles for user %q: %w", args[i], err)
|
||||
errs = multierror.Append(errs, updateErr)
|
||||
printer.PrintError(updateErr.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("System admin role assigned to user %q. Current roles are: %s", args[i], strings.Join(roles, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
return errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
func rolesMemberCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
||||
var errs *multierror.Error
|
||||
users := getUsersFromUserArgs(c, args)
|
||||
for i, user := range users {
|
||||
if user == nil {
|
||||
userErr := fmt.Errorf("unable to find user %q", args[i])
|
||||
errs = multierror.Append(errs, userErr)
|
||||
printer.PrintError(userErr.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
shouldRemoveSysadmin := false
|
||||
var newRoles []string
|
||||
|
||||
roles := strings.FieldsSeq(user.Roles)
|
||||
for role := range roles {
|
||||
switch role {
|
||||
case model.SystemAdminRoleId:
|
||||
shouldRemoveSysadmin = true
|
||||
default:
|
||||
newRoles = append(newRoles, role)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldRemoveSysadmin {
|
||||
if _, err := c.UpdateUserRoles(context.TODO(), user.Id, strings.Join(newRoles, " ")); err != nil {
|
||||
updateErr := fmt.Errorf("can't update roles for user %q: %w", args[i], err)
|
||||
errs = multierror.Append(errs, updateErr)
|
||||
printer.PrintError(updateErr.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("System admin role revoked for user %q. Current roles are: %s", args[i], strings.Join(newRoles, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
return errs.ErrorOrNil()
|
||||
}
|
||||
44
server/cmd/mmctl/commands/roles_list_e2e_test.go
Normal file
44
server/cmd/mmctl/commands/roles_list_e2e_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestRolesListCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.RunForSystemAdminAndLocal("List all available roles", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := rolesListCmdF(c, &cobra.Command{}, []string{})
|
||||
s.Require().Nil(err)
|
||||
|
||||
lines := printer.GetLines()
|
||||
s.Require().NotEmpty(lines, "Should return at least one role")
|
||||
|
||||
// Verify all returned items are valid Role objects with names
|
||||
for i, line := range lines {
|
||||
role, ok := line.(*model.Role)
|
||||
s.Require().True(ok, "Line %d should be a *model.Role object", i)
|
||||
s.Require().NotEmpty(role.Name, "Role %d should have a non-empty name", i)
|
||||
}
|
||||
|
||||
s.Len(printer.GetErrorLines(), 0, "Should not have any error output")
|
||||
})
|
||||
|
||||
s.Run("List roles without permissions should fail", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := rolesListCmdF(s.th.Client, &cobra.Command{}, []string{})
|
||||
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "failed to get roles")
|
||||
})
|
||||
}
|
||||
86
server/cmd/mmctl/commands/roles_list_test.go
Normal file
86
server/cmd/mmctl/commands/roles_list_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestRolesListPlain() {
|
||||
s.Run("Prints all role names", func() {
|
||||
printer.Clean()
|
||||
|
||||
roles := []*model.Role{
|
||||
{Id: "r1", Name: "system_admin"},
|
||||
{Id: "r2", Name: "system_user"},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetAllRoles(context.TODO()).
|
||||
Return(roles, &model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := rolesListCmdF(s.client, nil, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
lines := printer.GetLines()
|
||||
s.Require().NotEmpty(lines)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestRolesListJSON() {
|
||||
s.Run("prints JSON list of all role objects", func() {
|
||||
printer.Clean()
|
||||
|
||||
roles := []*model.Role{
|
||||
{Id: "r1", Name: "system_admin"},
|
||||
{Id: "r2", Name: "system_user"},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetAllRoles(context.TODO()).
|
||||
Return(roles, &model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
viper.Set("json", true)
|
||||
defer viper.Set("json", false)
|
||||
|
||||
err := rolesListCmdF(s.client, nil, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
lines := printer.GetLines()
|
||||
s.Require().NotEmpty(lines)
|
||||
|
||||
_, jerr := json.Marshal(lines)
|
||||
s.Require().NoError(jerr)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestRolesListPermissionError() {
|
||||
s.Run("Test permission denied message for getting roles", func() {
|
||||
printer.Clean()
|
||||
|
||||
forbiddenErr := fmt.Errorf("forbidden")
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetAllRoles(context.TODO()).
|
||||
Return(nil, &model.Response{StatusCode: http.StatusForbidden}, forbiddenErr).
|
||||
Times(1)
|
||||
|
||||
err := rolesListCmdF(s.client, nil, nil)
|
||||
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "failed to get roles")
|
||||
})
|
||||
}
|
||||
222
server/cmd/mmctl/commands/roles_test.go
Normal file
222
server/cmd/mmctl/commands/roles_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestMakeAdminCmd() {
|
||||
s.Run("Add admin privileges to user", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{Id: "1", Email: "u1@example.com", Roles: "system_user"}
|
||||
newRoles := "system_user system_admin"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), mockUser.Email, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateUserRoles(context.TODO(), mockUser.Id, newRoles).
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := rolesSystemAdminCmdF(s.client, &cobra.Command{}, []string{mockUser.Email})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(fmt.Sprintf("System admin role assigned to user %q. Current roles are: %s", mockUser.Email, "system_user, system_admin"), printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Adding admin privileges to existing admin", func() {
|
||||
printer.Clean()
|
||||
|
||||
roles := "system_user system_admin"
|
||||
mockUser := &model.User{Id: "1", Email: "u1@example.com", Roles: roles}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), mockUser.Email, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := rolesSystemAdminCmdF(s.client, &cobra.Command{}, []string{mockUser.Email})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Add admin to non existing user", func() {
|
||||
printer.Clean()
|
||||
|
||||
emailArg := "doesnotexist@example.com"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), emailArg, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), emailArg, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), emailArg, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := rolesSystemAdminCmdF(s.client, &cobra.Command{}, []string{emailArg})
|
||||
s.Require().ErrorContains(err, "unable to find user")
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find user %q", emailArg), printer.GetErrorLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Error while updating admin role", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{Id: "1", Email: "u1@example.com", Roles: "system_user"}
|
||||
newRoles := "system_user system_admin"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), mockUser.Email, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateUserRoles(context.TODO(), mockUser.Id, newRoles).
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := rolesSystemAdminCmdF(s.client, &cobra.Command{}, []string{mockUser.Email})
|
||||
s.Require().ErrorContains(err, "can't update roles for user")
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("can't update roles for user %q", mockUser.Email))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestMakeMemberCmd() {
|
||||
s.Run("Remove admin privileges for admin", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{Id: "1", Email: "u1@example.com", Roles: "system_user system_admin"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), mockUser.Email, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateUserRoles(context.TODO(), mockUser.Id, "system_user").
|
||||
Return(&model.Response{StatusCode: http.StatusOK}, nil).
|
||||
Times(1)
|
||||
|
||||
err := rolesMemberCmdF(s.client, &cobra.Command{}, []string{mockUser.Email})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(fmt.Sprintf("System admin role revoked for user %q. Current roles are: %s", mockUser.Email, "system_user"), printer.GetLines()[0])
|
||||
})
|
||||
|
||||
s.Run("Remove admin privileges from non admin user", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{Id: "1", Email: "u1@example.com", Roles: "system_user"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), mockUser.Email, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := rolesMemberCmdF(s.client, &cobra.Command{}, []string{mockUser.Email})
|
||||
s.Require().Nil(err)
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Error while revoking admin role", func() {
|
||||
printer.Clean()
|
||||
|
||||
mockUser := &model.User{Id: "1", Email: "u1@example.com", Roles: "system_user system_admin"}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), mockUser.Email, "").
|
||||
Return(mockUser, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
UpdateUserRoles(context.TODO(), mockUser.Id, "system_user").
|
||||
Return(&model.Response{StatusCode: http.StatusBadRequest}, errors.New("mock error")).
|
||||
Times(1)
|
||||
|
||||
err := rolesMemberCmdF(s.client, &cobra.Command{}, []string{mockUser.Email})
|
||||
s.Require().ErrorContains(err, "can't update roles for user")
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Contains(printer.GetErrorLines()[0], fmt.Sprintf("can't update roles for user %q", mockUser.Email))
|
||||
})
|
||||
|
||||
s.Run("Remove admin from non existing user", func() {
|
||||
printer.Clean()
|
||||
|
||||
emailArg := "doesnotexist@example.com"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByEmail(context.TODO(), emailArg, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUserByUsername(context.TODO(), emailArg, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
GetUser(context.TODO(), emailArg, "").
|
||||
Return(nil, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := rolesMemberCmdF(s.client, &cobra.Command{}, []string{emailArg})
|
||||
s.Require().ErrorContains(err, "unable to find user")
|
||||
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 1)
|
||||
s.Require().Equal(fmt.Sprintf("unable to find user %q", emailArg), printer.GetErrorLines()[0])
|
||||
})
|
||||
}
|
||||
123
server/cmd/mmctl/commands/root.go
Normal file
123
server/cmd/mmctl/commands/root.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
func Run(args []string) error {
|
||||
viper.SetEnvPrefix("mmctl")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
viper.SetDefault("local-socket-path", model.LocalModeSocketPath)
|
||||
viper.AutomaticEnv()
|
||||
|
||||
RootCmd.PersistentFlags().String("config", filepath.Join(xdgConfigHomeVar, configParent, configFileName), "path to the configuration file")
|
||||
_ = viper.BindPFlag("config", RootCmd.PersistentFlags().Lookup("config"))
|
||||
RootCmd.PersistentFlags().Bool("suppress-warnings", false, "disables printing warning messages")
|
||||
_ = viper.BindPFlag("suppress-warnings", RootCmd.PersistentFlags().Lookup("suppress-warnings"))
|
||||
RootCmd.PersistentFlags().String("format", "plain", "the format of the command output [plain, json]")
|
||||
_ = viper.BindPFlag("format", RootCmd.PersistentFlags().Lookup("format"))
|
||||
_ = RootCmd.PersistentFlags().MarkHidden("format")
|
||||
RootCmd.PersistentFlags().Bool("json", false, "the output format will be in json format")
|
||||
_ = viper.BindPFlag("json", RootCmd.PersistentFlags().Lookup("json"))
|
||||
RootCmd.PersistentFlags().Bool("strict", false, "will only run commands if the mmctl version matches the server one")
|
||||
_ = viper.BindPFlag("strict", RootCmd.PersistentFlags().Lookup("strict"))
|
||||
RootCmd.PersistentFlags().Bool("insecure-sha1-intermediate", false, "allows to use insecure TLS protocols, such as SHA-1")
|
||||
_ = viper.BindPFlag("insecure-sha1-intermediate", RootCmd.PersistentFlags().Lookup("insecure-sha1-intermediate"))
|
||||
RootCmd.PersistentFlags().Bool("insecure-tls-version", false, "allows to use TLS versions 1.0 and 1.1")
|
||||
_ = viper.BindPFlag("insecure-tls-version", RootCmd.PersistentFlags().Lookup("insecure-tls-version"))
|
||||
RootCmd.PersistentFlags().Bool("local", false, "allows communicating with the server through a unix socket")
|
||||
_ = viper.BindPFlag("local", RootCmd.PersistentFlags().Lookup("local"))
|
||||
RootCmd.PersistentFlags().Bool("short-stat", false, "short stat will provide useful statistical data")
|
||||
_ = RootCmd.PersistentFlags().MarkHidden("short-stat")
|
||||
RootCmd.PersistentFlags().Bool("no-stat", false, "the statistical data won't be displayed")
|
||||
_ = RootCmd.PersistentFlags().MarkHidden("no-stat")
|
||||
RootCmd.PersistentFlags().Bool("disable-pager", false, "disables paged output")
|
||||
_ = viper.BindPFlag("disable-pager", RootCmd.PersistentFlags().Lookup("disable-pager"))
|
||||
RootCmd.PersistentFlags().Bool("quiet", false, "prevent mmctl to generate output for the commands")
|
||||
_ = viper.BindPFlag("quiet", RootCmd.PersistentFlags().Lookup("quiet"))
|
||||
|
||||
RootCmd.SetArgs(args)
|
||||
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
printPanic(x)
|
||||
|
||||
_ = printer.Flush()
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
err := RootCmd.Execute()
|
||||
// Flush the printer first before printing any error
|
||||
_ = printer.Flush()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func printPanic(x any) {
|
||||
u, err := url.Parse("https://github.com/mattermost/mattermost/issues/new")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
q.Add("title", "[mmctl] [bug] panic on v"+Version)
|
||||
q.Add("body", "<!--- Please provide the stack trace -->\n")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
printer.PrintError("Uh oh! Something unexpected happened :( Would you mind reporting it?")
|
||||
printer.PrintError(u.String() + "\n")
|
||||
printer.PrintError(fmt.Sprintf("%s", x))
|
||||
printer.PrintError(string(debug.Stack()))
|
||||
}
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "mmctl",
|
||||
Short: "Remote client for the Open Source, self-hosted Slack-alternative",
|
||||
Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`,
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
for i, arg := range args {
|
||||
args[i] = strings.TrimSpace(arg)
|
||||
}
|
||||
format := viper.GetString("format")
|
||||
if viper.GetBool("disable-pager") {
|
||||
printer.OverrideEnablePager(false)
|
||||
}
|
||||
|
||||
printer.SetCommand(cmd)
|
||||
isJSON := viper.GetBool("json")
|
||||
if isJSON || format == printer.FormatJSON {
|
||||
printer.SetFormat(printer.FormatJSON)
|
||||
} else {
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
}
|
||||
quiet := viper.GetBool("quiet")
|
||||
printer.SetQuiet(quiet)
|
||||
|
||||
perPage, err := cmd.Flags().GetInt("per-page")
|
||||
if err == nil && perPage > MaxPageSize {
|
||||
printer.PrintError(fmt.Sprintf("Per page value is greater than the maximum allowed. Mattermost might only return %d items.", MaxPageSize))
|
||||
}
|
||||
},
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
58
server/cmd/mmctl/commands/root_test.go
Normal file
58
server/cmd/mmctl/commands/root_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func executeRawCommand(root *cobra.Command, args string) (c *cobra.Command, output string, err error) {
|
||||
actual := new(bytes.Buffer)
|
||||
RootCmd.SetOut(actual)
|
||||
RootCmd.SetErr(actual)
|
||||
RootCmd.SetArgs(strings.Split(args, " "))
|
||||
c, err = RootCmd.ExecuteC()
|
||||
return c, actual.String(), err
|
||||
}
|
||||
|
||||
func TestRootRecover(t *testing.T) {
|
||||
printPanic("some panic")
|
||||
|
||||
lines := printer.GetErrorLines()
|
||||
assert.Equal(t, "Uh oh! Something unexpected happened :( Would you mind reporting it?", lines[0])
|
||||
assert.True(t, strings.HasPrefix(lines[1], "https://github.com/mattermost/mattermost/issues/new?body="))
|
||||
assert.Equal(t, "some panic", lines[2])
|
||||
assert.True(t, strings.HasPrefix(lines[3], "goroutine "))
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestArgumentsHaveWhitespaceTrimmed() {
|
||||
arguments := []string{"value_1", "value_2"}
|
||||
lineEndings := []string{"\n", "\r", "\r\n"}
|
||||
prettyNames := []string{"-n", "-r", "-r-n"}
|
||||
commandCalled := false
|
||||
|
||||
for i, lineEnding := range lineEndings {
|
||||
testName := fmt.Sprintf("Commands have their arguments stripped of whitespace[%s]", prettyNames[i])
|
||||
s.Run(testName, func() {
|
||||
commandCalled = false
|
||||
commandFunction := func(command *cobra.Command, args []string) {
|
||||
commandCalled = true
|
||||
s.Equal(arguments, args, "Expected arguments to have their whitespace trimmed")
|
||||
}
|
||||
mockCommand := &cobra.Command{Use: "test", Run: commandFunction}
|
||||
commandString := strings.Join([]string{"test", " ", arguments[0], lineEnding, " ", arguments[1], lineEnding}, "")
|
||||
RootCmd.AddCommand(mockCommand)
|
||||
_, _, err := executeRawCommand(RootCmd, commandString)
|
||||
s.Require().NoError(err)
|
||||
s.Require().True(commandCalled, "Expected mock command to be called")
|
||||
})
|
||||
}
|
||||
}
|
||||
75
server/cmd/mmctl/commands/saml.go
Normal file
75
server/cmd/mmctl/commands/saml.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var SamlCmd = &cobra.Command{
|
||||
Use: "saml",
|
||||
Short: "SAML related utilities",
|
||||
}
|
||||
|
||||
var SamlAuthDataResetCmd = &cobra.Command{
|
||||
Use: "auth-data-reset",
|
||||
Short: "Reset AuthData field to Email",
|
||||
Long: "Resets the AuthData field for SAML users to their email. Run this utility after setting the 'id' SAML attribute to an empty value.",
|
||||
Example: ` # Reset all SAML users' AuthData field to their email, including deleted users
|
||||
$ mmctl saml auth-data-reset --include-deleted
|
||||
|
||||
# Show how many users would be affected by the reset
|
||||
$ mmctl saml auth-data-reset --dry-run
|
||||
|
||||
# Skip confirmation for resetting the AuthData
|
||||
$ mmctl saml auth-data-reset -y
|
||||
|
||||
# Only reset the AuthData for the following SAML users
|
||||
$ mmctl saml auth-data-reset --users userid1,userid2`,
|
||||
RunE: withClient(samlAuthDataResetCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
SamlAuthDataResetCmd.Flags().Bool("include-deleted", false, "Include deleted users")
|
||||
SamlAuthDataResetCmd.Flags().Bool("dry-run", false, "Dry run only")
|
||||
SamlAuthDataResetCmd.Flags().BoolP("yes", "y", false, "Skip confirmation")
|
||||
SamlAuthDataResetCmd.Flags().StringSlice("users", nil, "Comma-separated list of user IDs to which the operation will be applied")
|
||||
|
||||
SamlCmd.AddCommand(
|
||||
SamlAuthDataResetCmd,
|
||||
)
|
||||
RootCmd.AddCommand(SamlCmd)
|
||||
}
|
||||
|
||||
func samlAuthDataResetCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
includeDeleted, _ := cmd.Flags().GetBool("include-deleted")
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
confirmed, _ := cmd.Flags().GetBool("yes")
|
||||
userIDs, _ := cmd.Flags().GetStringSlice("users")
|
||||
|
||||
if !dryRun && !confirmed {
|
||||
if err := getConfirmation("This action is irreversible. Are you sure you want to continue?", false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
numAffected, _, err := c.ResetSamlAuthDataToEmail(context.TODO(), includeDeleted, dryRun, userIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
printer.Print(fmt.Sprintf("%d user records would be affected.\n", numAffected))
|
||||
} else {
|
||||
printer.Print(fmt.Sprintf("%d user records were changed.\n", numAffected))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
80
server/cmd/mmctl/commands/saml_test.go
Normal file
80
server/cmd/mmctl/commands/saml_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestSamlAuthDataReset() {
|
||||
s.Run("Reset auth data without confirmation returns an error", func() {
|
||||
cmd := &cobra.Command{}
|
||||
err := samlAuthDataResetCmdF(s.client, cmd, nil)
|
||||
s.Require().NotNil(err)
|
||||
s.Require().EqualError(err, "could not proceed, either enable --confirm flag or use an interactive shell to complete operation: this is not an interactive shell")
|
||||
})
|
||||
|
||||
s.Run("Reset auth data without errors", func() {
|
||||
printer.Clean()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("yes", true, "")
|
||||
outputMessage := "1 user records were changed.\n"
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ResetSamlAuthDataToEmail(context.TODO(), false, false, []string{}).
|
||||
Return(int64(1), &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := samlAuthDataResetCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], outputMessage)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Reset auth data dry run", func() {
|
||||
printer.Clean()
|
||||
outputMessage := "1 user records would be affected.\n"
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("dry-run", true, "")
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ResetSamlAuthDataToEmail(context.TODO(), false, true, []string{}).
|
||||
Return(int64(1), &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := samlAuthDataResetCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], outputMessage)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.Run("Reset auth data with specified users", func() {
|
||||
printer.Clean()
|
||||
users := []string{"user1"}
|
||||
s.client.
|
||||
EXPECT().
|
||||
ResetSamlAuthDataToEmail(context.TODO(), false, false, users).
|
||||
Return(int64(1), &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("yes", true, "")
|
||||
cmd.Flags().StringSlice("users", users, "")
|
||||
|
||||
err := samlAuthDataResetCmdF(s.client, cmd, nil)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
412
server/cmd/mmctl/commands/sampledata.go
Normal file
412
server/cmd/mmctl/commands/sampledata.go
Normal file
@@ -0,0 +1,412 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//nolint:gosec
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/imports"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
pUtils "github.com/mattermost/mattermost/server/public/utils"
|
||||
|
||||
"github.com/icrowley/fake"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
deactivatedUser = "deactivated"
|
||||
guestUser = "guest"
|
||||
attachmentsDir = "attachments"
|
||||
)
|
||||
|
||||
var SampledataCmd = &cobra.Command{
|
||||
Use: "sampledata",
|
||||
Short: "Generate sample data",
|
||||
Long: "Generate a sample data file and store it locally, or directly import it to the remote server",
|
||||
Example: ` # you can create a sampledata file and store it locally
|
||||
$ mmctl sampledata --bulk sampledata-file.jsonl
|
||||
|
||||
# or you can simply print it to the stdout
|
||||
$ mmctl sampledata --bulk -
|
||||
|
||||
# the amount of entities to create can be customized
|
||||
$ mmctl sampledata -t 7 -u 20 -g 4
|
||||
|
||||
# the sampledata file can be directly imported in the remote server by not specifying a --bulk flag
|
||||
$ mmctl sampledata
|
||||
|
||||
# and the sample users can be created with profile pictures
|
||||
$ mmctl sampledata --profile-images ./images/profiles`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(sampledataCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
SampledataCmd.Flags().Int64P("seed", "s", 1, "Seed used for generating the random data (Different seeds generate different data).")
|
||||
SampledataCmd.Flags().IntP("teams", "t", 2, "The number of sample teams.")
|
||||
SampledataCmd.Flags().Int("channels-per-team", 10, "The number of sample channels per team.")
|
||||
SampledataCmd.Flags().IntP("users", "u", 15, "The number of sample users.")
|
||||
SampledataCmd.Flags().IntP("guests", "g", 1, "The number of sample guests.")
|
||||
SampledataCmd.Flags().Int("deactivated-users", 0, "The number of deactivated users.")
|
||||
SampledataCmd.Flags().Int("team-memberships", 2, "The number of sample team memberships per user.")
|
||||
SampledataCmd.Flags().Int("channel-memberships", 5, "The number of sample channel memberships per user in a team.")
|
||||
SampledataCmd.Flags().Int("posts-per-channel", 100, "The number of sample post per channel.")
|
||||
SampledataCmd.Flags().Int("direct-channels", 30, "The number of sample direct message channels.")
|
||||
SampledataCmd.Flags().Int("posts-per-direct-channel", 15, "The number of sample posts per direct message channel.")
|
||||
SampledataCmd.Flags().Int("group-channels", 15, "The number of sample group message channels.")
|
||||
SampledataCmd.Flags().Int("posts-per-group-channel", 30, "The number of sample posts per group message channel.")
|
||||
SampledataCmd.Flags().String("profile-images", "", "Optional. Path to folder with images to randomly pick as user profile image.")
|
||||
SampledataCmd.Flags().StringP("bulk", "b", "", "Optional. Path to write a JSONL bulk file instead of uploading into the remote server.")
|
||||
|
||||
RootCmd.AddCommand(SampledataCmd)
|
||||
}
|
||||
|
||||
func uploadAndProcess(c client.Client, zipPath string, isLocal bool) error {
|
||||
zipFile, err := os.Open(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open import file %q: %w", zipPath, err)
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
info, err := zipFile.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat import file: %w", err)
|
||||
}
|
||||
|
||||
userID := "me"
|
||||
if isLocal {
|
||||
userID = model.UploadNoUserID
|
||||
}
|
||||
|
||||
// create session
|
||||
us, _, err := c.CreateUpload(context.TODO(), &model.UploadSession{
|
||||
Filename: info.Name(),
|
||||
FileSize: info.Size(),
|
||||
Type: model.UploadTypeImport,
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create upload session: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Upload session successfully created, ID: {{.Id}} ", us)
|
||||
|
||||
// upload file
|
||||
finfo, _, err := c.UploadData(context.TODO(), us.Id, zipFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload data: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Import file successfully uploaded, name: {{.Name}}", finfo)
|
||||
|
||||
// process
|
||||
job, _, err := c.CreateJob(context.TODO(), &model.Job{
|
||||
Type: model.JobTypeImportProcess,
|
||||
Data: map[string]string{
|
||||
"import_file": us.Id + "_" + finfo.Name,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create import process job: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Import process job successfully created, ID: {{.Id}}", job)
|
||||
|
||||
for {
|
||||
job, _, err = c.GetJob(context.TODO(), job.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get import job status: %w", err)
|
||||
}
|
||||
|
||||
if job.Status != model.JobStatusPending && job.Status != model.JobStatusInProgress {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
if job.Status != model.JobStatusSuccess {
|
||||
return fmt.Errorf("job reported non-success status: %s", job.Status)
|
||||
}
|
||||
|
||||
printer.PrintT("Sampledata successfully processed", job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processProfileImagesDir(profileImagesPath, tmpDir, bulk string) ([]string, error) {
|
||||
profileImages := []string{}
|
||||
var profileImagesStat os.FileInfo
|
||||
profileImagesStat, err := os.Stat(profileImagesPath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("profile images folder doesn't exist")
|
||||
}
|
||||
if !profileImagesStat.IsDir() {
|
||||
return nil, fmt.Errorf("profile-images parameters must be a directory")
|
||||
}
|
||||
|
||||
profileImagesFiles, err := os.ReadDir(profileImagesPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid profile-images parameter: %w", err)
|
||||
}
|
||||
|
||||
// we need to copy the images to be part of the import zip
|
||||
if bulk == "" {
|
||||
for _, profileImage := range profileImagesFiles {
|
||||
profileImageSrc := filepath.Join(profileImagesPath, profileImage.Name())
|
||||
profileImagePath := filepath.Join(attachmentsDir, profileImage.Name())
|
||||
profileImageDst := filepath.Join(tmpDir, profileImagePath)
|
||||
if err := pUtils.CopyFile(profileImageSrc, profileImageDst); err != nil {
|
||||
return nil, fmt.Errorf("cannot copy file %q to %q: %w", profileImageSrc, profileImageDst, err)
|
||||
}
|
||||
// the path we use in the profile info is relative to the zipfile base
|
||||
profileImages = append(profileImages, profileImagePath)
|
||||
}
|
||||
// we're not importing the resulting file, so we keep the
|
||||
// image paths corresponding to the value of the flag
|
||||
} else {
|
||||
for _, profileImage := range profileImagesFiles {
|
||||
profileImages = append(profileImages, filepath.Join(profileImagesPath, profileImage.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(profileImages)
|
||||
return profileImages, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func sampledataCmdF(c client.Client, command *cobra.Command, args []string) error {
|
||||
seed, _ := command.Flags().GetInt64("seed")
|
||||
bulk, _ := command.Flags().GetString("bulk")
|
||||
teams, _ := command.Flags().GetInt("teams")
|
||||
channelsPerTeam, _ := command.Flags().GetInt("channels-per-team")
|
||||
users, _ := command.Flags().GetInt("users")
|
||||
deactivatedUsers, _ := command.Flags().GetInt("deactivated-users")
|
||||
guests, _ := command.Flags().GetInt("guests")
|
||||
teamMemberships, _ := command.Flags().GetInt("team-memberships")
|
||||
channelMemberships, _ := command.Flags().GetInt("channel-memberships")
|
||||
postsPerChannel, _ := command.Flags().GetInt("posts-per-channel")
|
||||
directChannels, _ := command.Flags().GetInt("direct-channels")
|
||||
postsPerDirectChannel, _ := command.Flags().GetInt("posts-per-direct-channel")
|
||||
groupChannels, _ := command.Flags().GetInt("group-channels")
|
||||
postsPerGroupChannel, _ := command.Flags().GetInt("posts-per-group-channel")
|
||||
profileImagesPath, _ := command.Flags().GetString("profile-images")
|
||||
withAttachments := profileImagesPath != ""
|
||||
|
||||
if teamMemberships > teams {
|
||||
return fmt.Errorf("you can't have more team memberships than teams")
|
||||
}
|
||||
if channelMemberships > channelsPerTeam {
|
||||
return fmt.Errorf("you can't have more channel memberships than channels per team")
|
||||
}
|
||||
|
||||
if users < 6 && groupChannels > 0 {
|
||||
return fmt.Errorf("you can't have group channels generation with less than 6 users. Use --group-channels 0 or increase the number of users")
|
||||
}
|
||||
|
||||
var bulkFile *os.File
|
||||
var tmpDir string
|
||||
var err error
|
||||
switch bulk {
|
||||
case "":
|
||||
tmpDir, err = os.MkdirTemp("", "mmctl-sampledata-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create temporary directory")
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if withAttachments {
|
||||
if err = os.Mkdir(filepath.Join(tmpDir, attachmentsDir), 0755); err != nil {
|
||||
return fmt.Errorf("cannot create attachments directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
bulkFile, err = os.Create(filepath.Join(tmpDir, "import.jsonl"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open temporary file: %w", err)
|
||||
}
|
||||
defer bulkFile.Close()
|
||||
case "-":
|
||||
bulkFile = os.Stdout
|
||||
default:
|
||||
bulkFile, err = os.OpenFile(bulk, os.O_RDWR|os.O_CREATE, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write into the %q file: %w", bulk, err)
|
||||
}
|
||||
defer bulkFile.Close()
|
||||
}
|
||||
|
||||
profileImages := []string{}
|
||||
if profileImagesPath != "" {
|
||||
profileImages, err = processProfileImagesDir(profileImagesPath, tmpDir, bulk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot process profile images directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(bulkFile)
|
||||
version := 1
|
||||
if err := encoder.Encode(imports.LineImportData{Type: "version", Version: &version}); err != nil {
|
||||
return fmt.Errorf("could not encode version line: %w", err)
|
||||
}
|
||||
|
||||
fake.Seed(seed)
|
||||
rand.Seed(seed)
|
||||
|
||||
teamsAndChannels := make(map[string][]string, teams)
|
||||
for i := range teams {
|
||||
teamLine := createTeam(i)
|
||||
teamsAndChannels[*teamLine.Team.Name] = []string{}
|
||||
if err := encoder.Encode(teamLine); err != nil {
|
||||
return fmt.Errorf("could not encode team line: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
teamsList := make([]string, len(teamsAndChannels))
|
||||
teamsListIndex := 0
|
||||
for teamName := range teamsAndChannels {
|
||||
teamsList[teamsListIndex] = teamName
|
||||
teamsListIndex++
|
||||
}
|
||||
slices.Sort(teamsList)
|
||||
|
||||
for _, teamName := range teamsList {
|
||||
for i := range channelsPerTeam {
|
||||
channelLine := createChannel(i, teamName)
|
||||
teamsAndChannels[teamName] = append(teamsAndChannels[teamName], *channelLine.Channel.Name)
|
||||
if err := encoder.Encode(channelLine); err != nil {
|
||||
return fmt.Errorf("could not encode channel line: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allUsers := make([]string, users+guests+deactivatedUsers)
|
||||
allUsersIndex := 0
|
||||
for i := range users {
|
||||
userLine := createUser(i, teamMemberships, channelMemberships, teamsAndChannels, profileImages, "")
|
||||
if err := encoder.Encode(userLine); err != nil {
|
||||
return fmt.Errorf("cannot encode user line: %w", err)
|
||||
}
|
||||
allUsers[allUsersIndex] = *userLine.User.Username
|
||||
allUsersIndex++
|
||||
}
|
||||
for i := range guests {
|
||||
userLine := createUser(i, teamMemberships, channelMemberships, teamsAndChannels, profileImages, guestUser)
|
||||
if err := encoder.Encode(userLine); err != nil {
|
||||
return fmt.Errorf("cannot encode user line: %w", err)
|
||||
}
|
||||
allUsers[allUsersIndex] = *userLine.User.Username
|
||||
allUsersIndex++
|
||||
}
|
||||
for i := range deactivatedUsers {
|
||||
userLine := createUser(i, teamMemberships, channelMemberships, teamsAndChannels, profileImages, deactivatedUser)
|
||||
if err := encoder.Encode(userLine); err != nil {
|
||||
return fmt.Errorf("cannot encode user line: %w", err)
|
||||
}
|
||||
allUsers[allUsersIndex] = *userLine.User.Username
|
||||
allUsersIndex++
|
||||
}
|
||||
|
||||
for team, channels := range teamsAndChannels {
|
||||
for _, channel := range channels {
|
||||
dates := sortedRandomDates(postsPerChannel)
|
||||
|
||||
for i := range postsPerChannel {
|
||||
postLine := createPost(team, channel, allUsers, dates[i])
|
||||
if err := encoder.Encode(postLine); err != nil {
|
||||
return fmt.Errorf("cannot encode post line: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for range directChannels {
|
||||
user1 := allUsers[rand.Intn(len(allUsers))]
|
||||
user2 := allUsers[rand.Intn(len(allUsers))]
|
||||
channelLine := createDirectChannel([]string{user1, user2})
|
||||
if err := encoder.Encode(channelLine); err != nil {
|
||||
return fmt.Errorf("cannot encode channel line: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for range directChannels {
|
||||
user1 := allUsers[rand.Intn(len(allUsers))]
|
||||
user2 := allUsers[rand.Intn(len(allUsers))]
|
||||
|
||||
dates := sortedRandomDates(postsPerDirectChannel)
|
||||
for j := range postsPerDirectChannel {
|
||||
postLine := createDirectPost([]string{user1, user2}, dates[j])
|
||||
if err := encoder.Encode(postLine); err != nil {
|
||||
return fmt.Errorf("cannot encode post line: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for range groupChannels {
|
||||
users := []string{}
|
||||
totalUsers := 3 + rand.Intn(3)
|
||||
for len(users) < totalUsers {
|
||||
user := allUsers[rand.Intn(len(allUsers))]
|
||||
if !slices.Contains(users, user) {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
channelLine := createDirectChannel(users)
|
||||
if err := encoder.Encode(channelLine); err != nil {
|
||||
return fmt.Errorf("cannot encode channel line: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for range groupChannels {
|
||||
users := []string{}
|
||||
totalUsers := 3 + rand.Intn(3)
|
||||
for len(users) < totalUsers {
|
||||
user := allUsers[rand.Intn(len(allUsers))]
|
||||
if !slices.Contains(users, user) {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
|
||||
dates := sortedRandomDates(postsPerGroupChannel)
|
||||
for j := range postsPerGroupChannel {
|
||||
postLine := createDirectPost(users, dates[j])
|
||||
if err := encoder.Encode(postLine); err != nil {
|
||||
return fmt.Errorf("cannot encode post line: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're writing to stdout, we can finish here
|
||||
if bulk == "-" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if bulk == "" {
|
||||
zipPath := filepath.Join(os.TempDir(), "mmctl-sampledata.zip")
|
||||
defer os.Remove(zipPath)
|
||||
|
||||
if err := zipDir(zipPath, tmpDir); err != nil {
|
||||
return fmt.Errorf("cannot compress %q directory into zipfile: %w", tmpDir, err)
|
||||
}
|
||||
|
||||
isLocal, _ := command.Flags().GetBool("local")
|
||||
if err := uploadAndProcess(c, zipPath, isLocal); err != nil {
|
||||
return fmt.Errorf("cannot upload and process zipfile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
63
server/cmd/mmctl/commands/sampledata_test.go
Normal file
63
server/cmd/mmctl/commands/sampledata_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestSampledataCmd() {
|
||||
s.Run("should fail because you have more team memberships than teams", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("teams", 10, "")
|
||||
cmd.Flags().Int("team-memberships", 11, "")
|
||||
err := sampledataCmdF(s.client, cmd, []string{})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "more team memberships than teams")
|
||||
})
|
||||
|
||||
s.Run("should fail because you have more channel memberships than channels per team", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("channels-per-team", 10, "")
|
||||
cmd.Flags().Int("channel-memberships", 11, "")
|
||||
err := sampledataCmdF(s.client, cmd, []string{})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "more channel memberships than channels per team")
|
||||
})
|
||||
|
||||
s.Run("should fail because you have group channels and don't have enough users (6 users)", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("group-channels", 1, "")
|
||||
cmd.Flags().Int("users", 5, "")
|
||||
err := sampledataCmdF(s.client, cmd, []string{})
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), "group channels generation with less than 6 users")
|
||||
})
|
||||
|
||||
s.Run("should not fail with less than 6 users and no group channels", func() {
|
||||
printer.Clean()
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "mmctl-sampledata-test-")
|
||||
s.Require().NoError(err)
|
||||
tmpFile.Close()
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("bulk", tmpFile.Name(), "")
|
||||
cmd.Flags().Int("group-channels", 0, "")
|
||||
cmd.Flags().Int("users", 5, "")
|
||||
err = sampledataCmdF(s.client, cmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
}
|
||||
460
server/cmd/mmctl/commands/sampledata_util.go
Normal file
460
server/cmd/mmctl/commands/sampledata_util.go
Normal file
@@ -0,0 +1,460 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//nolint:gosec
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/imports"
|
||||
|
||||
"github.com/icrowley/fake"
|
||||
)
|
||||
|
||||
func randomPastTime(seconds int) int64 {
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.FixedZone("UTC", 0))
|
||||
return (today.Unix() * 1000) - int64(rand.Intn(seconds*1000))
|
||||
}
|
||||
|
||||
func sortedRandomDates(size int) []int64 {
|
||||
dates := make([]int64, size)
|
||||
for i := range size {
|
||||
dates[i] = randomPastTime(50000)
|
||||
}
|
||||
slices.Sort(dates)
|
||||
return dates
|
||||
}
|
||||
|
||||
func randomEmoji() string {
|
||||
emojis := []string{"+1", "-1", "heart", "blush"}
|
||||
return emojis[rand.Intn(len(emojis))]
|
||||
}
|
||||
|
||||
func randomReaction(users []string, parentCreateAt int64) app.ReactionImportData {
|
||||
user := users[rand.Intn(len(users))]
|
||||
emoji := randomEmoji()
|
||||
date := parentCreateAt + int64(rand.Intn(100000))
|
||||
return app.ReactionImportData{
|
||||
User: &user,
|
||||
EmojiName: &emoji,
|
||||
CreateAt: &date,
|
||||
}
|
||||
}
|
||||
|
||||
func randomReply(users []string, parentCreateAt int64) imports.ReplyImportData {
|
||||
user := users[rand.Intn(len(users))]
|
||||
message := randomMessage(users)
|
||||
date := parentCreateAt + int64(rand.Intn(100000))
|
||||
return imports.ReplyImportData{
|
||||
User: &user,
|
||||
Message: &message,
|
||||
CreateAt: &date,
|
||||
}
|
||||
}
|
||||
|
||||
func randomMessage(users []string) string {
|
||||
var message string
|
||||
switch rand.Intn(30) {
|
||||
case 0:
|
||||
mention := users[rand.Intn(len(users))]
|
||||
message = "@" + mention + " " + fake.Sentence()
|
||||
case 1:
|
||||
switch rand.Intn(2) {
|
||||
case 0:
|
||||
mattermostVideos := []string{"Q4MgnxbpZas", "BFo7E9-Kc_E", "LsMLR-BHsKg", "MRmGDhlMhNA", "mUOPxT7VgWc"}
|
||||
message = "https://www.youtube.com/watch?v=" + mattermostVideos[rand.Intn(len(mattermostVideos))]
|
||||
case 1:
|
||||
mattermostTweets := []string{"943119062334353408", "949370809528832005", "948539688171819009", "939122439115681792", "938061722027425797"}
|
||||
message = "https://twitter.com/mattermosthq/status/" + mattermostTweets[rand.Intn(len(mattermostTweets))]
|
||||
}
|
||||
case 2:
|
||||
message = ""
|
||||
if rand.Intn(2) == 0 {
|
||||
message += fake.Sentence()
|
||||
}
|
||||
lenList := rand.Intn(4) + 1
|
||||
for range lenList {
|
||||
message += "\n * " + fake.Word()
|
||||
}
|
||||
default:
|
||||
if rand.Intn(2) == 0 {
|
||||
message = fake.Sentence()
|
||||
} else {
|
||||
message = fake.Paragraph()
|
||||
}
|
||||
if rand.Intn(3) == 0 {
|
||||
message += "\n" + fake.Sentence()
|
||||
}
|
||||
if rand.Intn(3) == 0 {
|
||||
message += "\n" + fake.Sentence()
|
||||
}
|
||||
if rand.Intn(3) == 0 {
|
||||
message += "\n" + fake.Sentence()
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func createUser(idx int, teamMemberships int, channelMemberships int, teamsAndChannels map[string][]string, profileImages []string, userType string) imports.LineImportData {
|
||||
firstName := fake.FirstName()
|
||||
lastName := fake.LastName()
|
||||
position := fake.JobTitle()
|
||||
|
||||
username := fmt.Sprintf("%s.%s", strings.ToLower(firstName), strings.ToLower(lastName))
|
||||
roles := "system_user"
|
||||
|
||||
var password string
|
||||
var email string
|
||||
|
||||
switch userType {
|
||||
case guestUser:
|
||||
password = fmt.Sprintf("SampleGu@st-%03d", idx)
|
||||
email = fmt.Sprintf("guest-%d@sample.mattermost.com", idx)
|
||||
roles = "system_guest"
|
||||
if idx == 0 {
|
||||
username = "guest"
|
||||
email = "guest@sample.mattermost.com"
|
||||
}
|
||||
case deactivatedUser:
|
||||
password = fmt.Sprintf("SampleDe@ctivated-%d", idx)
|
||||
email = fmt.Sprintf("deactivated-%d@sample.mattermost.com", idx)
|
||||
default:
|
||||
password = fmt.Sprintf("SampleUs@r-%03d", idx)
|
||||
email = fmt.Sprintf("user-%d@sample.mattermost.com", idx)
|
||||
if idx == 0 {
|
||||
username = "sysadmin"
|
||||
password = "Sys@dmin-sample1"
|
||||
email = "sysadmin@sample.mattermost.com"
|
||||
} else if idx == 1 {
|
||||
username = "user-1"
|
||||
}
|
||||
|
||||
if idx%5 == 0 {
|
||||
roles = "system_admin system_user"
|
||||
}
|
||||
}
|
||||
|
||||
// The 75% of the users have custom profile image
|
||||
var profileImage *string
|
||||
if rand.Intn(4) != 0 {
|
||||
profileImageSelector := rand.Int()
|
||||
if len(profileImages) > 0 {
|
||||
profileImage = &profileImages[profileImageSelector%len(profileImages)]
|
||||
}
|
||||
}
|
||||
|
||||
useMilitaryTime := "false"
|
||||
if idx != 0 && rand.Intn(2) == 0 {
|
||||
useMilitaryTime = "true"
|
||||
}
|
||||
|
||||
collapsePreviews := "false"
|
||||
if idx != 0 && rand.Intn(2) == 0 {
|
||||
collapsePreviews = "true"
|
||||
}
|
||||
|
||||
messageDisplay := "clean"
|
||||
if idx != 0 && rand.Intn(2) == 0 {
|
||||
messageDisplay = "compact"
|
||||
}
|
||||
|
||||
channelDisplayMode := "full"
|
||||
if idx != 0 && rand.Intn(2) == 0 {
|
||||
channelDisplayMode = "centered"
|
||||
}
|
||||
|
||||
// Some users has nickname
|
||||
nickname := ""
|
||||
if rand.Intn(5) == 0 {
|
||||
nickname = fake.Company()
|
||||
}
|
||||
|
||||
// sysadmin, user-1 and user-2 users skip tutorial steps
|
||||
// Other half of users also skip tutorial steps
|
||||
tutorialStep := "999"
|
||||
if idx > 2 {
|
||||
switch rand.Intn(6) {
|
||||
case 1:
|
||||
tutorialStep = "1"
|
||||
case 2:
|
||||
tutorialStep = "2"
|
||||
case 3:
|
||||
tutorialStep = "3"
|
||||
}
|
||||
}
|
||||
|
||||
teams := []imports.UserTeamImportData{}
|
||||
possibleTeams := []string{}
|
||||
for teamName := range teamsAndChannels {
|
||||
possibleTeams = append(possibleTeams, teamName)
|
||||
}
|
||||
slices.Sort(possibleTeams)
|
||||
for range teamMemberships {
|
||||
if len(possibleTeams) == 0 {
|
||||
break
|
||||
}
|
||||
position := rand.Intn(len(possibleTeams))
|
||||
team := possibleTeams[position]
|
||||
possibleTeams = append(possibleTeams[:position], possibleTeams[position+1:]...)
|
||||
if teamChannels, err := teamsAndChannels[team]; err {
|
||||
teams = append(teams, createTeamMembership(channelMemberships, teamChannels, &team, userType == guestUser))
|
||||
}
|
||||
}
|
||||
|
||||
var deleteAt int64
|
||||
if userType == deactivatedUser {
|
||||
deleteAt = model.GetMillis()
|
||||
}
|
||||
|
||||
user := imports.UserImportData{
|
||||
Avatar: imports.Avatar{
|
||||
ProfileImage: profileImage,
|
||||
},
|
||||
Username: &username,
|
||||
Email: &email,
|
||||
Password: &password,
|
||||
Nickname: &nickname,
|
||||
FirstName: &firstName,
|
||||
LastName: &lastName,
|
||||
Position: &position,
|
||||
Roles: &roles,
|
||||
Teams: &teams,
|
||||
UseMilitaryTime: &useMilitaryTime,
|
||||
CollapsePreviews: &collapsePreviews,
|
||||
MessageDisplay: &messageDisplay,
|
||||
ChannelDisplayMode: &channelDisplayMode,
|
||||
TutorialStep: &tutorialStep,
|
||||
DeleteAt: &deleteAt,
|
||||
}
|
||||
return imports.LineImportData{
|
||||
Type: "user",
|
||||
User: &user,
|
||||
}
|
||||
}
|
||||
|
||||
func createTeamMembership(numOfchannels int, teamChannels []string, teamName *string, guest bool) imports.UserTeamImportData {
|
||||
roles := "team_user"
|
||||
if guest {
|
||||
roles = "team_guest"
|
||||
} else if rand.Intn(5) == 0 {
|
||||
roles = "team_user team_admin"
|
||||
}
|
||||
channels := []imports.UserChannelImportData{}
|
||||
teamChannelsCopy := append([]string(nil), teamChannels...)
|
||||
for range numOfchannels {
|
||||
if len(teamChannelsCopy) == 0 {
|
||||
break
|
||||
}
|
||||
position := rand.Intn(len(teamChannelsCopy))
|
||||
channelName := teamChannelsCopy[position]
|
||||
teamChannelsCopy = append(teamChannelsCopy[:position], teamChannelsCopy[position+1:]...)
|
||||
channels = append(channels, createChannelMembership(channelName, guest))
|
||||
}
|
||||
|
||||
return imports.UserTeamImportData{
|
||||
Name: teamName,
|
||||
Roles: &roles,
|
||||
Channels: &channels,
|
||||
}
|
||||
}
|
||||
|
||||
func createChannelMembership(channelName string, guest bool) imports.UserChannelImportData {
|
||||
roles := "channel_user"
|
||||
if guest {
|
||||
roles = "channel_guest"
|
||||
} else if rand.Intn(5) == 0 {
|
||||
roles = "channel_user channel_admin"
|
||||
}
|
||||
favorite := rand.Intn(5) == 0
|
||||
|
||||
return imports.UserChannelImportData{
|
||||
Name: &channelName,
|
||||
Roles: &roles,
|
||||
Favorite: &favorite,
|
||||
}
|
||||
}
|
||||
|
||||
func getSampleTeamName(idx int) string {
|
||||
for {
|
||||
name := fmt.Sprintf("%s-%d", fake.Word(), idx)
|
||||
if !model.IsReservedTeamName(name) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createTeam(idx int) imports.LineImportData {
|
||||
displayName := fake.Word()
|
||||
name := getSampleTeamName(idx)
|
||||
allowOpenInvite := rand.Intn(2) == 0
|
||||
|
||||
description := fake.Paragraph()
|
||||
if len(description) > 255 {
|
||||
description = description[0:255]
|
||||
}
|
||||
|
||||
teamType := "O"
|
||||
if rand.Intn(2) == 0 {
|
||||
teamType = "I"
|
||||
}
|
||||
|
||||
team := imports.TeamImportData{
|
||||
DisplayName: &displayName,
|
||||
Name: &name,
|
||||
AllowOpenInvite: &allowOpenInvite,
|
||||
Description: &description,
|
||||
Type: &teamType,
|
||||
}
|
||||
return imports.LineImportData{
|
||||
Type: "team",
|
||||
Team: &team,
|
||||
}
|
||||
}
|
||||
|
||||
func createChannel(idx int, teamName string) imports.LineImportData {
|
||||
displayName := fake.Word()
|
||||
name := fmt.Sprintf("%s-%d", fake.Word(), idx)
|
||||
header := fake.Paragraph()
|
||||
purpose := fake.Paragraph()
|
||||
|
||||
if len(purpose) > 250 {
|
||||
purpose = purpose[0:250]
|
||||
}
|
||||
|
||||
channelType := model.ChannelTypePrivate
|
||||
if rand.Intn(2) == 0 {
|
||||
channelType = model.ChannelTypeOpen
|
||||
}
|
||||
|
||||
channel := imports.ChannelImportData{
|
||||
Team: &teamName,
|
||||
Name: &name,
|
||||
DisplayName: &displayName,
|
||||
Type: &channelType,
|
||||
Header: &header,
|
||||
Purpose: &purpose,
|
||||
}
|
||||
return imports.LineImportData{
|
||||
Type: "channel",
|
||||
Channel: &channel,
|
||||
}
|
||||
}
|
||||
|
||||
func createPost(team string, channel string, allUsers []string, createAt int64) imports.LineImportData {
|
||||
message := randomMessage(allUsers)
|
||||
user := allUsers[rand.Intn(len(allUsers))]
|
||||
|
||||
// Some messages are flagged by a user
|
||||
flaggedBy := []string{}
|
||||
if rand.Intn(10) == 0 {
|
||||
flaggedBy = append(flaggedBy, allUsers[rand.Intn(len(allUsers))])
|
||||
}
|
||||
|
||||
reactions := []app.ReactionImportData{}
|
||||
if rand.Intn(10) == 0 {
|
||||
for {
|
||||
reactions = append(reactions, randomReaction(allUsers, createAt))
|
||||
if rand.Intn(3) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replies := []imports.ReplyImportData{}
|
||||
if rand.Intn(10) == 0 {
|
||||
for {
|
||||
replies = append(replies, randomReply(allUsers, createAt))
|
||||
if rand.Intn(4) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post := imports.PostImportData{
|
||||
Team: &team,
|
||||
Channel: &channel,
|
||||
User: &user,
|
||||
Message: &message,
|
||||
CreateAt: &createAt,
|
||||
FlaggedBy: &flaggedBy,
|
||||
Reactions: &reactions,
|
||||
Replies: &replies,
|
||||
}
|
||||
return imports.LineImportData{
|
||||
Type: "post",
|
||||
Post: &post,
|
||||
}
|
||||
}
|
||||
|
||||
func createDirectChannel(members []string) imports.LineImportData {
|
||||
header := fake.Sentence()
|
||||
var p []*imports.DirectChannelMemberImportData
|
||||
|
||||
for _, m := range members {
|
||||
p = append(p, &imports.DirectChannelMemberImportData{
|
||||
Username: new(m),
|
||||
})
|
||||
}
|
||||
|
||||
channel := imports.DirectChannelImportData{
|
||||
Participants: p,
|
||||
Header: &header,
|
||||
}
|
||||
return imports.LineImportData{
|
||||
Type: "direct_channel",
|
||||
DirectChannel: &channel,
|
||||
}
|
||||
}
|
||||
|
||||
func createDirectPost(members []string, createAt int64) imports.LineImportData {
|
||||
message := randomMessage(members)
|
||||
user := members[rand.Intn(len(members))]
|
||||
|
||||
// Some messages are flagged by an user
|
||||
flaggedBy := []string{}
|
||||
if rand.Intn(10) == 0 {
|
||||
flaggedBy = append(flaggedBy, members[rand.Intn(len(members))])
|
||||
}
|
||||
|
||||
reactions := []app.ReactionImportData{}
|
||||
if rand.Intn(10) == 0 {
|
||||
for {
|
||||
reactions = append(reactions, randomReaction(members, createAt))
|
||||
if rand.Intn(3) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replies := []imports.ReplyImportData{}
|
||||
if rand.Intn(10) == 0 {
|
||||
for {
|
||||
replies = append(replies, randomReply(members, createAt))
|
||||
if rand.Intn(4) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post := imports.DirectPostImportData{
|
||||
ChannelMembers: &members,
|
||||
User: &user,
|
||||
Message: &message,
|
||||
CreateAt: &createAt,
|
||||
FlaggedBy: &flaggedBy,
|
||||
Reactions: &reactions,
|
||||
Replies: &replies,
|
||||
}
|
||||
return imports.LineImportData{
|
||||
Type: "direct_post",
|
||||
DirectPost: &post,
|
||||
}
|
||||
}
|
||||
37
server/cmd/mmctl/commands/sampledata_util_test.go
Normal file
37
server/cmd/mmctl/commands/sampledata_util_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Verifies all sampledata passwords meet the FIPS minimum length (PasswordFIPSMinimumLength).
|
||||
func TestCreateUserPasswordLength(t *testing.T) {
|
||||
const minLen = model.PasswordFIPSMinimumLength
|
||||
|
||||
indices := []int{0, 1, 9, 10, 99, 100, 999, 1000}
|
||||
userTypes := []string{guestUser, deactivatedUser, ""}
|
||||
|
||||
for _, userType := range userTypes {
|
||||
for _, idx := range indices {
|
||||
name := userType
|
||||
if name == "" {
|
||||
name = "user"
|
||||
}
|
||||
t.Run(fmt.Sprintf("%s/idx=%d", name, idx), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
data := createUser(idx, 0, 0, map[string][]string{}, nil, userType)
|
||||
pwd := *data.User.Password
|
||||
require.GreaterOrEqualf(t, len(pwd), minLen,
|
||||
"password %q (userType=%q idx=%d) is shorter than FIPS minimum %d",
|
||||
pwd, userType, idx, minLen)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
257
server/cmd/mmctl/commands/system.go
Normal file
257
server/cmd/mmctl/commands/system.go
Normal file
@@ -0,0 +1,257 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
)
|
||||
|
||||
var SystemCmd = &cobra.Command{
|
||||
Use: "system",
|
||||
Short: "System management",
|
||||
Long: `System management commands for interacting with the server state and configuration.`,
|
||||
}
|
||||
|
||||
var SystemGetBusyCmd = &cobra.Command{
|
||||
Use: "getbusy",
|
||||
Short: "Get the current busy state",
|
||||
Long: `Gets the server busy state (high load) and timestamp corresponding to when the server busy flag will be automatically cleared.`,
|
||||
Example: ` system getbusy`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(getBusyCmdF),
|
||||
}
|
||||
|
||||
var SystemSetBusyCmd = &cobra.Command{
|
||||
Use: "setbusy -s [seconds]",
|
||||
Short: "Set the busy state to true",
|
||||
Long: `Set the busy state to true for the specified number of seconds, which disables non-critical services.`,
|
||||
Example: ` system setbusy -s 3600`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(setBusyCmdF),
|
||||
}
|
||||
|
||||
var SystemClearBusyCmd = &cobra.Command{
|
||||
Use: "clearbusy",
|
||||
Short: "Clears the busy state",
|
||||
Long: `Clear the busy state, which re-enables non-critical services.`,
|
||||
Example: ` system clearbusy`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(clearBusyCmdF),
|
||||
}
|
||||
|
||||
var SystemVersionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Prints the remote server version",
|
||||
Long: "Prints the server version of the currently connected Mattermost instance",
|
||||
Example: ` system version`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(systemVersionCmdF),
|
||||
}
|
||||
|
||||
var SystemStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Prints the status of the server",
|
||||
Long: "Prints the server status calculated using several basic server healthchecks",
|
||||
Example: ` system status`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(systemStatusCmdF),
|
||||
}
|
||||
|
||||
var SystemSupportPacketCmd = &cobra.Command{
|
||||
Use: "supportpacket",
|
||||
Short: "Download a Support Packet",
|
||||
Long: "Generate and download a Support Packet of the server to share it with Mattermost Support",
|
||||
Example: ` system supportpacket`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(systemSupportPacketCmdF),
|
||||
}
|
||||
|
||||
var SystemNukeCmd = &cobra.Command{
|
||||
Use: "nuke",
|
||||
Short: "Destructive operations that permanently delete data",
|
||||
Long: "Destructive operations that permanently and irreversibly delete server data. Use with extreme caution.",
|
||||
}
|
||||
|
||||
var SystemNukeUsersCmd = &cobra.Command{
|
||||
Use: "users",
|
||||
Short: "Delete all users and all posts. Local command only.",
|
||||
Long: "Permanently delete all users and all related information including posts. This command can only be run in local mode.",
|
||||
Example: " system nuke users",
|
||||
Args: cobra.NoArgs,
|
||||
PreRun: localOnlyPrecheck,
|
||||
RunE: withClient(nukeUsersCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
SystemSetBusyCmd.Flags().UintP("seconds", "s", 3600, "Number of seconds until server is automatically marked as not busy.")
|
||||
_ = SystemSetBusyCmd.MarkFlagRequired("seconds")
|
||||
|
||||
SystemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Define the output file name")
|
||||
|
||||
SystemNukeUsersCmd.Flags().Bool("confirm", false, "Confirm you really want to permanently delete all users, posts, and related data, and that a DB backup has been performed")
|
||||
|
||||
SystemNukeCmd.AddCommand(
|
||||
SystemNukeUsersCmd,
|
||||
)
|
||||
|
||||
SystemCmd.AddCommand(
|
||||
SystemGetBusyCmd,
|
||||
SystemSetBusyCmd,
|
||||
SystemClearBusyCmd,
|
||||
SystemVersionCmd,
|
||||
SystemStatusCmd,
|
||||
SystemSupportPacketCmd,
|
||||
SystemNukeCmd,
|
||||
)
|
||||
RootCmd.AddCommand(SystemCmd)
|
||||
}
|
||||
|
||||
func getBusyCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
sbs, _, err := c.GetServerBusy(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get busy state: %w", err)
|
||||
}
|
||||
printer.PrintT("busy:{{.Busy}} expires:{{.Expires_ts}}", sbs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBusyCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
seconds, err := cmd.Flags().GetUint("seconds")
|
||||
if err != nil || seconds == 0 {
|
||||
return errors.New("seconds must be a number > 0")
|
||||
}
|
||||
|
||||
_, err = c.SetServerBusy(context.TODO(), int(seconds))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to set busy state: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Busy state set", map[string]string{"status": "ok"})
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearBusyCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
_, err := c.ClearServerBusy(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to clear busy state: %w", err)
|
||||
}
|
||||
printer.PrintT("Busy state cleared", map[string]string{"status": "ok"})
|
||||
return nil
|
||||
}
|
||||
|
||||
func systemVersionCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
||||
printer.SetSingle(true)
|
||||
// server version information comes with all responses. We can't
|
||||
// use the initial "withClient" connection information as local
|
||||
// mode doesn't need to log in, so we use an endpoint that will
|
||||
// always return a valid response
|
||||
_, resp, err := c.GetPing(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch server version: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Server version {{.version}}", map[string]string{"version": resp.ServerVersion})
|
||||
return nil
|
||||
}
|
||||
|
||||
func systemStatusCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
status, _, err := c.GetPingWithOptions(context.TODO(), model.SystemPingOptions{
|
||||
FullStatus: true,
|
||||
RESTSemantics: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch server status: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT(`Server status: {{.status}}
|
||||
Android Latest Version: {{.AndroidLatestVersion}}
|
||||
Android Minimum Version: {{.AndroidMinVersion}}
|
||||
Desktop Latest Version: {{.DesktopLatestVersion}}
|
||||
Desktop Minimum Version: {{.DesktopMinVersion}}
|
||||
Ios Latest Version: {{.IosLatestVersion}}
|
||||
Ios Minimum Version: {{.IosMinVersion}}
|
||||
Database Status: {{.database_status}}
|
||||
Filestore Status: {{.filestore_status}}`, status)
|
||||
|
||||
// Check health status and return non-zero exit code if any component is unhealthy
|
||||
if status["status"] != model.StatusOk {
|
||||
return fmt.Errorf("server status is unhealthy: %s", status["status"])
|
||||
}
|
||||
if dbStatus, ok := status["database_status"]; ok && dbStatus != model.StatusOk {
|
||||
return fmt.Errorf("database status is unhealthy: %s", dbStatus)
|
||||
}
|
||||
if filestoreStatus, ok := status["filestore_status"]; ok && filestoreStatus != model.StatusOk {
|
||||
return fmt.Errorf("filestore status is unhealthy: %s", filestoreStatus)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func systemSupportPacketCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
||||
printer.SetSingle(true)
|
||||
|
||||
filename, err := cmd.Flags().GetString("output-file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer.Print("Downloading Support Packet")
|
||||
|
||||
data, rFilename, _, err := c.GenerateSupportPacket(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch Support Packet: %w", err)
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
filename = rFilename
|
||||
}
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create zip file: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(file, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to zip file: %w", err)
|
||||
}
|
||||
|
||||
printer.PrintT("Downloaded Support Packet to {{ .filename }}", map[string]string{"filename": filename})
|
||||
return nil
|
||||
}
|
||||
|
||||
func nukeUsersCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
confirmFlag, _ := cmd.Flags().GetBool("confirm")
|
||||
if !confirmFlag {
|
||||
if err := getConfirmation("Are you sure you want to permanently delete all users, posts, and related data?", true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := c.PermanentDeleteAllUsers(context.TODO()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer printer.Print("All users successfully deleted")
|
||||
|
||||
return nil
|
||||
}
|
||||
250
server/cmd/mmctl/commands/system_e2e_test.go
Normal file
250
server/cmd/mmctl/commands/system_e2e_test.go
Normal file
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
||||
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestGetBusyCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
s.th.App.Srv().Platform().Busy.Set(time.Minute)
|
||||
defer s.th.App.Srv().Platform().Busy.Clear()
|
||||
|
||||
s.Run("MM-T3979 Should fail when regular user attempts to get server busy status", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := getBusyCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3956 Get server busy status", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := getBusyCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
state, ok := printer.GetLines()[0].(*model.ServerBusyState)
|
||||
s.Require().True(ok, true)
|
||||
s.Require().True(state.Busy, true)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestNukeUsersCmd() {
|
||||
s.SetupTestHelper().InitBasic(s.T())
|
||||
|
||||
s.Run("Delete all users as unprivileged user should not work", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
confirm := true
|
||||
cmd.Flags().BoolVar(&confirm, "confirm", confirm, "confirm")
|
||||
|
||||
err := nukeUsersCmdF(s.th.Client, cmd, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// expect users not deleted
|
||||
users, err := s.th.App.GetUsersPage(&model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
}, true)
|
||||
s.Require().Nil(err)
|
||||
s.Require().NotZero(len(users))
|
||||
})
|
||||
|
||||
s.Run("Delete all users as system admin through the port API should not work", func() {
|
||||
printer.Clean()
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
confirm := true
|
||||
cmd.Flags().BoolVar(&confirm, "confirm", confirm, "confirm")
|
||||
|
||||
err := nukeUsersCmdF(s.th.SystemAdminClient, cmd, []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Len(printer.GetLines(), 0)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
|
||||
// expect users not deleted
|
||||
users, err := s.th.App.GetUsersPage(&model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
}, true)
|
||||
s.Require().Nil(err)
|
||||
s.Require().NotZero(len(users))
|
||||
})
|
||||
|
||||
s.Run("Delete all users through local mode should work correctly", func() {
|
||||
printer.Clean()
|
||||
|
||||
// populate with some user
|
||||
for range 10 {
|
||||
userData := model.User{
|
||||
Username: "fakeuser" + model.NewRandomString(10),
|
||||
Password: model.NewTestPassword(),
|
||||
Email: s.th.GenerateTestEmail(),
|
||||
}
|
||||
_, err := s.th.App.CreateUser(s.th.Context, &userData)
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
confirm := true
|
||||
cmd.Flags().BoolVar(&confirm, "confirm", confirm, "confirm")
|
||||
|
||||
// delete all users only works on local mode
|
||||
err := nukeUsersCmdF(s.th.LocalClient, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Len(printer.GetLines(), 1)
|
||||
s.Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Equal(printer.GetLines()[0], "All users successfully deleted")
|
||||
|
||||
// expect users deleted
|
||||
users, err := s.th.App.GetUsersPage(&model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
}, true)
|
||||
s.Require().Nil(err)
|
||||
s.Require().Zero(len(users))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestSetBusyCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
s.th.App.Srv().Platform().Busy.Clear()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Uint("seconds", 60, "")
|
||||
|
||||
s.Run("MM-T3980 Should fail when regular user attempts to set server busy status", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := setBusyCmdF(s.th.Client, cmd, nil)
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3957 Set server status to busy", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := setBusyCmdF(c, cmd, nil)
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
s.th.App.Srv().Platform().Busy.Clear()
|
||||
s.Require().False(s.th.App.Srv().Platform().Busy.IsBusy())
|
||||
}()
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], map[string]string{"status": "ok"})
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().True(s.th.App.Srv().Platform().Busy.IsBusy())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestClearBusyCmd() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
s.th.App.Srv().Platform().Busy.Set(time.Minute)
|
||||
defer s.th.App.Srv().Platform().Busy.Clear()
|
||||
|
||||
s.Run("MM-T3981 Should fail when regular user attempts to clear server busy status", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := clearBusyCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||
s.Require().Error(err)
|
||||
s.Require().Len(printer.GetLines(), 0)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
|
||||
s.RunForSystemAdminAndLocal("MM-T3958 Clear server status to busy", func(c client.Client) {
|
||||
printer.Clean()
|
||||
|
||||
err := clearBusyCmdF(c, &cobra.Command{}, nil)
|
||||
s.Require().NoError(err)
|
||||
defer func() {
|
||||
s.th.App.Srv().Platform().Busy.Set(time.Minute)
|
||||
s.Require().True(s.th.App.Srv().Platform().Busy.IsBusy())
|
||||
}()
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Equal(printer.GetLines()[0], map[string]string{"status": "ok"})
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().False(s.th.App.Srv().Platform().Busy.IsBusy())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
|
||||
s.SetupEnterpriseTestHelper().InitBasic(s.T())
|
||||
|
||||
printer.SetFormat(printer.FormatPlain)
|
||||
s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) })
|
||||
|
||||
s.Run("Download Support Packet with default filename", func() {
|
||||
printer.Clean()
|
||||
|
||||
err := systemSupportPacketCmdF(s.th.SystemAdminClient, SystemSupportPacketCmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet")
|
||||
s.Require().Contains(printer.GetLines()[1], "Downloaded Support Packet to ")
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
|
||||
var found bool
|
||||
|
||||
entries, err := os.ReadDir(".")
|
||||
s.Require().NoError(err)
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), "mm_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
|
||||
b, err := os.ReadFile(e.Name())
|
||||
s.NoError(err)
|
||||
|
||||
s.NotEmpty(b, b)
|
||||
|
||||
s.T().Cleanup(func() {
|
||||
err = os.Remove(e.Name())
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
found = true
|
||||
}
|
||||
}
|
||||
s.True(found)
|
||||
})
|
||||
|
||||
s.Run("Download Support Packet with custom filename", func() {
|
||||
printer.Clean()
|
||||
|
||||
systemSupportPacketCmd := &cobra.Command{}
|
||||
systemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Define the output file name")
|
||||
err := systemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
|
||||
s.Require().NoError(err)
|
||||
|
||||
defer func() {
|
||||
s.Require().NoError(os.Remove("foo.zip"))
|
||||
}()
|
||||
|
||||
err = systemSupportPacketCmdF(s.th.SystemAdminClient, systemSupportPacketCmd, []string{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet")
|
||||
s.Require().Equal(printer.GetLines()[1], "Downloaded Support Packet to foo.zip")
|
||||
|
||||
b, err := os.ReadFile("foo.zip")
|
||||
s.Require().NoError(err)
|
||||
s.NotNil(b, b)
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user