[MM-67880] Add /mobile-logs slash command (#35658)

* [MM-67880] Add /mobile-logs slash command with E2E tests

Add a new /mobile-logs slash command that allows users to manage the
attach_app_logs preference for themselves or other users (admin-only).
Includes unit tests for all code paths and Playwright E2E tests covering
self-management, admin cross-user management, permission denial, and
error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix i18n error key suffixes and format E2E tests

Rename error i18n keys to use .app_error suffix matching upstream
convention (no_permission, update_error, user_not_found). Run prettier
on the E2E test file to fix formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix E2E test type error: use getUserPreferences instead of getMyPreferences

The getAttachLogsPreference helper was using getMyPreferences() which returns
PreferenceType (not an array), causing TS2345 errors. Switch to
getUserPreferences(userId) which returns the expected array type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [MM-67880] Move unreachable usage fallback into switch default case

The return after the switch was unreachable because action is validated
earlier to be "on", "off", or "status". Move it into an explicit default
case with a defensive comment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* allow up to 2 arguments

* Add audit logging for mobile logs slash command actions

Implement a new function to log audit records when users enable or disable the attach_app_logs preference via the /mobile-logs command. This includes capturing relevant metadata such as user IDs, session information, and the action taken. The logging occurs in both the enable and disable command paths, enhancing traceability and accountability for user preference changes.

* Enhance mobile logs command to handle cross-user permission checks

Add a new response function for cases where a regular user attempts to access mobile log settings for another user, ensuring they receive a neutral error message instead of specific user information. Update the command logic to incorporate this response for both nonexistent users and deactivated accounts. Additionally, modify related tests and internationalization keys to reflect these changes, improving security and user experience.

* Update E2E test for /mobile-logs command to verify permission denial for nonexistent users

Enhance the existing E2E test for the /mobile-logs command by adding assertions to check that users receive a permission denial message when attempting to change mobile log settings for a nonexistent user. This improves test coverage and ensures proper error handling in the application.

* update i18n strings

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Guillermo Vayá
2026-04-20 12:46:36 +02:00
committed by GitHub
parent 0c99cc36cc
commit 8cd48d4651
5 changed files with 900 additions and 0 deletions
@@ -0,0 +1,320 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* Helper to find the attach_app_logs preference for a given user.
*/
async function getAttachLogsPreference(
userClient: {
getUserPreferences: (userId: string) => Promise<Array<{category: string; name: string; value: string}>>;
},
userId: string,
) {
const prefs = await userClient.getUserPreferences(userId);
return prefs.find(
(p: {category: string; name: string}) => p.category === 'advanced_settings' && p.name === 'attach_app_logs',
);
}
test.describe('/mobile-logs slash command', () => {
/**
* @objective Verify that /mobile-logs on sets the attach_app_logs preference to true
* and returns an ephemeral confirmation visible only to the invoking user.
*/
test(
'MM-T67880 enables mobile logs for self and confirms preference is set',
{tag: '@slash_commands'},
async ({pw}) => {
// # Initialize setup
const {team, user, userClient} = await pw.initSetup();
// # Log in as the user and navigate to town-square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Execute /mobile-logs on
await channelsPage.postMessage('/mobile-logs on');
// * Verify ephemeral response confirms logs enabled
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText('Mobile app log attachment is now enabled for you');
// * Verify the response is ephemeral (only visible to the user)
await expect(lastPost.container.locator('.post__visibility')).toContainText('(Only visible to you)');
// * Verify the preference was actually set via API
const logPref = await getAttachLogsPreference(userClient, user.id);
expect(logPref).toBeDefined();
expect(logPref!.value).toBe('true');
},
);
/**
* @objective Verify that /mobile-logs off sets the attach_app_logs preference to false
* and returns an ephemeral confirmation.
*/
test('MM-T67880 disables mobile logs for self after enabling via API', {tag: '@slash_commands'}, async ({pw}) => {
// # Initialize setup
const {team, user, userClient} = await pw.initSetup();
// # Pre-enable the preference via API to avoid back-to-back commands
await userClient.savePreferences(user.id, [
{user_id: user.id, category: 'advanced_settings', name: 'attach_app_logs', value: 'true'},
]);
// # Log in as the user and navigate to town-square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Execute /mobile-logs off
await channelsPage.postMessage('/mobile-logs off');
// * Verify ephemeral response confirms logs disabled
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText('Mobile app log attachment is now disabled for you');
// * Verify the preference was set to false via API
const logPref = await getAttachLogsPreference(userClient, user.id);
expect(logPref).toBeDefined();
expect(logPref!.value).toBe('false');
});
/**
* @objective Verify that /mobile-logs status reports disabled by default for a fresh user.
*/
test(
'MM-T67880 reports mobile logs status as disabled for a fresh user',
{tag: '@slash_commands'},
async ({pw}) => {
// # Initialize setup
const {team, user} = await pw.initSetup();
// # Log in as the user and navigate to town-square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Execute /mobile-logs status
await channelsPage.postMessage('/mobile-logs status');
// * Verify ephemeral response shows disabled
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText('Mobile app log attachment is currently disabled for you');
},
);
/**
* @objective Verify that /mobile-logs status reports enabled after the preference
* has been set to true via API.
*/
test(
'MM-T67880 reports mobile logs status as enabled after preference is set',
{tag: '@slash_commands'},
async ({pw}) => {
// # Initialize setup
const {team, user, userClient} = await pw.initSetup();
// # Pre-enable the preference via API
await userClient.savePreferences(user.id, [
{user_id: user.id, category: 'advanced_settings', name: 'attach_app_logs', value: 'true'},
]);
// # Log in as the user and navigate to town-square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Execute /mobile-logs status
await channelsPage.postMessage('/mobile-logs status');
// * Verify ephemeral response shows enabled
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText('Mobile app log attachment is currently enabled for you');
},
);
/**
* @objective Verify that /mobile-logs displays a usage hint when invoked
* without arguments or with an invalid action.
*/
test('MM-T67880 displays usage hint for invalid or missing arguments', {tag: '@slash_commands'}, async ({pw}) => {
// # Initialize setup
const {team, user} = await pw.initSetup();
// # Log in as the user and navigate to town-square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Execute /mobile-logs with an invalid action
await channelsPage.postMessage('/mobile-logs invalid');
// * Verify usage message is shown
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText('Usage: /mobile-logs [on|off|status] [@username]');
});
/**
* @objective Verify that a system admin can enable mobile logs for another user
* and the preference is persisted on the target user's account.
*/
test(
'MM-T67880 admin enables mobile logs for another user and preference is persisted',
{tag: '@slash_commands'},
async ({pw}) => {
// # Initialize setup
const {team, adminUser, user, userClient} = await pw.initSetup();
// # Log in as admin and navigate to town-square
const {channelsPage} = await pw.testBrowser.login(adminUser);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Execute /mobile-logs on for the regular user
await channelsPage.postMessage(`/mobile-logs on @${user.username}`);
// * Verify ephemeral response confirms logs enabled for the target user
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText(`Mobile app log attachment is now enabled for @${user.username}`);
// * Verify the preference was set on the target user via API
const logPref = await getAttachLogsPreference(userClient, user.id);
expect(logPref).toBeDefined();
expect(logPref!.value).toBe('true');
},
);
/**
* @objective Verify that a system admin can disable mobile logs for another user
* after they have been enabled.
*/
test(
'MM-T67880 admin disables mobile logs for another user after enabling via API',
{tag: '@slash_commands'},
async ({pw}) => {
// # Initialize setup
const {team, adminUser, user, userClient} = await pw.initSetup();
// # Pre-enable the preference for the target user via API
const {adminClient} = await pw.getAdminClient();
await adminClient.savePreferences(user.id, [
{user_id: user.id, category: 'advanced_settings', name: 'attach_app_logs', value: 'true'},
]);
// # Log in as admin and navigate to town-square
const {channelsPage} = await pw.testBrowser.login(adminUser);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Disable for the regular user
await channelsPage.postMessage(`/mobile-logs off @${user.username}`);
// * Verify ephemeral response confirms logs disabled
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText(`Mobile app log attachment is now disabled for @${user.username}`);
// * Verify the preference was set to false on the target user
const logPref = await getAttachLogsPreference(userClient, user.id);
expect(logPref).toBeDefined();
expect(logPref!.value).toBe('false');
},
);
/**
* @objective Verify that a non-admin user is denied when attempting to modify
* another user's mobile logs preference.
*/
test(
'MM-T67880 rejects mobile log modification when caller is not an admin',
{tag: '@slash_commands'},
async ({pw}) => {
// # Initialize setup with two regular users
const {team, adminClient, user} = await pw.initSetup();
const otherUser = await pw.random.user('other');
const {id: otherUserId} = await adminClient.createUser(otherUser, '', '');
await adminClient.addToTeam(team.id, otherUserId);
// # Log in as the first regular user
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Try to enable mobile logs for the other user
await channelsPage.postMessage(`/mobile-logs on @${otherUser.username}`);
// * Verify permission denied message
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText('Unable to change mobile log settings for that user.');
},
);
/**
* @objective Verify that a regular user cannot infer whether a username exists when
* attempting to target another account (same denial as missing permission).
*/
test(
'MM-T67880 regular user gets permission denial for nonexistent cross-user target',
{tag: '@slash_commands'},
async ({pw}) => {
const {team, user} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post the mobile-logs command
await channelsPage.postMessage('/mobile-logs on @nonexistentuser12345');
const lastPost = await channelsPage.getLastPost();
// * Expect permission denial message
await lastPost.toContainText('Unable to change mobile log settings for that user.');
},
);
/**
* @objective Verify that a system admin can check the mobile logs status for another
* user and it correctly reflects the current preference state.
*/
test('MM-T67880 admin checks mobile logs status for another user', {tag: '@slash_commands'}, async ({pw}) => {
// # Initialize setup
const {team, adminUser, user} = await pw.initSetup();
// # Log in as admin and navigate to town-square
const {channelsPage} = await pw.testBrowser.login(adminUser);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Check status (default off)
await channelsPage.postMessage(`/mobile-logs status @${user.username}`);
// * Verify status shows disabled
const statusOffPost = await channelsPage.getLastPost();
await statusOffPost.toContainText(`Mobile app log attachment is currently disabled for @${user.username}`);
});
/**
* @objective Verify that /mobile-logs returns a user-not-found error when targeting
* a nonexistent username.
*/
test('MM-T67880 returns error for nonexistent target user', {tag: '@slash_commands'}, async ({pw}) => {
// # Initialize setup
const {team, adminUser} = await pw.initSetup();
// # Log in as admin
const {channelsPage} = await pw.testBrowser.login(adminUser);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Try to enable for a nonexistent user
await channelsPage.postMessage('/mobile-logs on @nonexistentuser12345');
// * Verify user not found message
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText('Could not find user "nonexistentuser12345"');
});
});
@@ -0,0 +1,246 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"net/http"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/app"
)
type MobileLogsProvider struct {
}
const (
CmdMobileLogs = "mobile-logs"
)
func init() {
app.RegisterCommandProvider(&MobileLogsProvider{})
}
func mobileLogsCrossUserUnavailableResponse(args *model.CommandArgs) *model.CommandResponse {
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.cross_user_unavailable.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
func logMobileLogsAttachAppLogsAudit(a *app.App, rctx request.CTX, actorUserID, targetUserID, value string) {
rec := &model.AuditRecord{
EventName: model.AuditEventUpdatePreferences,
Status: model.AuditStatusSuccess,
Actor: model.AuditEventActor{
UserId: actorUserID,
SessionId: rctx.Session().Id,
Client: rctx.UserAgent(),
IpAddress: rctx.IPAddress(),
XForwardedFor: rctx.XForwardedFor(),
},
Meta: map[string]any{
model.AuditKeyAPIPath: rctx.Path(),
model.AuditKeyClusterID: a.GetClusterId(),
},
EventData: model.AuditEventData{
Parameters: map[string]any{
"source": "slash_command/" + CmdMobileLogs,
"target_user_id": targetUserID,
"preference_category": model.PreferenceCategoryAdvancedSettings,
"preference_name": model.PreferenceNameAttachAppLogs,
"value": value,
},
PriorState: map[string]any{},
ResultState: map[string]any{},
ObjectType: "user_preference",
},
}
a.LogAuditRecWithLevel(rctx, rec, app.LevelAPI, nil)
}
func (*MobileLogsProvider) GetTrigger() string {
return CmdMobileLogs
}
func (*MobileLogsProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMobileLogs,
AutoComplete: true,
AutoCompleteDesc: T("api.command_mobile_logs.desc"),
AutoCompleteHint: T("api.command_mobile_logs.hint"),
DisplayName: T("api.command_mobile_logs.name"),
}
}
func (*MobileLogsProvider) DoCommand(a *app.App, rctx request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
fields := strings.Fields(message)
if len(fields) == 0 || len(fields) > 2 {
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.usage"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
action := strings.ToLower(fields[0])
if action != "on" && action != "off" && action != "status" {
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.usage"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
targetUserID := args.UserId
targetDisplayName := args.T("api.command_mobile_logs.you")
if len(fields) > 1 {
username := strings.TrimPrefix(fields[1], "@")
username = strings.ToLower(username)
if !model.IsValidUsername(username) {
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.user_not_found.app_error", map[string]any{"Username": username}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
caller, appErr := a.GetUser(args.UserId)
if appErr != nil {
rctx.Logger().Error("Failed to get caller for mobile-logs command", mlog.String("user_id", args.UserId), mlog.Err(appErr))
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.update_error.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
isSelf := username == strings.ToLower(caller.Username)
if isSelf {
targetUserID = caller.Id
targetDisplayName = "@" + caller.Username
} else {
// Cross-user: callers without system admin get one neutral outcome for any failure
// (unknown user, deactivated, disallowed target, or missing role) to avoid username
// enumeration. System admins still get explicit not-found messages for support workflows.
if !a.HasPermissionTo(args.UserId, model.PermissionManageSystem) && !a.HasPermissionTo(args.UserId, model.PermissionEditOtherUsers) {
return mobileLogsCrossUserUnavailableResponse(args)
}
callerHasManageSystem := a.HasPermissionTo(args.UserId, model.PermissionManageSystem)
targetUser, lookupErr := a.GetUserByUsername(username)
if lookupErr != nil {
if lookupErr.StatusCode == http.StatusNotFound {
if callerHasManageSystem {
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.user_not_found.app_error", map[string]any{"Username": username}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return mobileLogsCrossUserUnavailableResponse(args)
}
rctx.Logger().Error("Failed to get user by username", mlog.String("username", username), mlog.Err(lookupErr))
if callerHasManageSystem {
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.user_not_found.app_error", map[string]any{"Username": username}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return mobileLogsCrossUserUnavailableResponse(args)
}
if targetUser.DeleteAt != 0 {
if callerHasManageSystem {
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.user_not_found.app_error", map[string]any{"Username": username}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return mobileLogsCrossUserUnavailableResponse(args)
}
targetUserID = targetUser.Id
targetDisplayName = "@" + targetUser.Username
if !callerHasManageSystem && targetUser.IsSystemAdmin() {
return mobileLogsCrossUserUnavailableResponse(args)
}
}
}
switch action {
case "on":
prefs := model.Preferences{
{
UserId: targetUserID,
Category: model.PreferenceCategoryAdvancedSettings,
Name: model.PreferenceNameAttachAppLogs,
Value: "true",
},
}
if err := a.UpdatePreferences(rctx, targetUserID, prefs); err != nil {
rctx.Logger().Error("Failed to update attach_app_logs preference", mlog.String("user_id", targetUserID), mlog.Err(err))
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.update_error.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
logMobileLogsAttachAppLogsAudit(a, rctx, args.UserId, targetUserID, "true")
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.enabled", map[string]any{"User": targetDisplayName}),
ResponseType: model.CommandResponseTypeEphemeral,
}
case "off":
prefs := model.Preferences{
{
UserId: targetUserID,
Category: model.PreferenceCategoryAdvancedSettings,
Name: model.PreferenceNameAttachAppLogs,
Value: "false",
},
}
if err := a.UpdatePreferences(rctx, targetUserID, prefs); err != nil {
rctx.Logger().Error("Failed to update attach_app_logs preference", mlog.String("user_id", targetUserID), mlog.Err(err))
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.update_error.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
logMobileLogsAttachAppLogsAudit(a, rctx, args.UserId, targetUserID, "false")
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.disabled", map[string]any{"User": targetDisplayName}),
ResponseType: model.CommandResponseTypeEphemeral,
}
case "status":
pref, err := a.GetPreferenceByCategoryAndNameForUser(rctx, targetUserID, model.PreferenceCategoryAdvancedSettings, model.PreferenceNameAttachAppLogs)
if err != nil {
rctx.Logger().Debug("Could not get attach_app_logs preference, defaulting to off", mlog.String("user_id", targetUserID), mlog.Err(err))
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.status_off", map[string]any{"User": targetDisplayName}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if pref.Value == "true" {
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.status_on", map[string]any{"User": targetDisplayName}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.status_off", map[string]any{"User": targetDisplayName}),
ResponseType: model.CommandResponseTypeEphemeral,
}
default:
// Defensive: action is already validated to be "on", "off", or "status" above.
return &model.CommandResponse{
Text: args.T("api.command_mobile_logs.usage"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
}
@@ -0,0 +1,284 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
)
func TestMobileLogsGetCommand(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
result := cmd.GetCommand(th.App, i18n.IdentityTfunc())
assert.Equal(t, CmdMobileLogs, result.Trigger)
assert.True(t, result.AutoComplete)
assert.NotEmpty(t, result.AutoCompleteDesc)
assert.NotEmpty(t, result.AutoCompleteHint)
assert.NotEmpty(t, result.DisplayName)
}
func TestMobileLogsOnSelf(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "on")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.enabled", resp.Text)
pref, err := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, model.PreferenceCategoryAdvancedSettings, model.PreferenceNameAttachAppLogs)
require.Nil(t, err)
assert.Equal(t, "true", pref.Value)
}
func TestMobileLogsOffSelf(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "on")
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "off")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.disabled", resp.Text)
pref, err := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, model.PreferenceCategoryAdvancedSettings, model.PreferenceNameAttachAppLogs)
require.Nil(t, err)
assert.Equal(t, "false", pref.Value)
}
func TestMobileLogsStatusDefault(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "status")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.status_off", resp.Text)
}
func TestMobileLogsStatusOn(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "on")
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "status")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.status_on", resp.Text)
}
func TestMobileLogsOnOtherUserWithPermission(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "on @"+th.BasicUser.Username)
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.enabled", resp.Text)
pref, err := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, model.PreferenceCategoryAdvancedSettings, model.PreferenceNameAttachAppLogs)
require.Nil(t, err)
assert.Equal(t, "true", pref.Value)
}
func TestMobileLogsOffOtherUserWithPermission(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "on @"+th.BasicUser.Username)
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "off @"+th.BasicUser.Username)
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.disabled", resp.Text)
pref, err := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, model.PreferenceCategoryAdvancedSettings, model.PreferenceNameAttachAppLogs)
require.Nil(t, err)
assert.Equal(t, "false", pref.Value)
}
func TestMobileLogsStatusOtherUser(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
// Default status for other user
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "status @"+th.BasicUser.Username)
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.status_off", resp.Text)
// Enable for other user, then check status
cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "on @"+th.BasicUser.Username)
resp = cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "status @"+th.BasicUser.Username)
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.status_on", resp.Text)
}
func TestMobileLogsOnOtherUserWithoutPermission(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "on @"+th.BasicUser2.Username)
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.cross_user_unavailable.app_error", resp.Text)
}
func TestMobileLogsOnOtherUserWithoutAtPrefix(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "on "+th.BasicUser.Username)
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.enabled", resp.Text)
pref, err := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, model.PreferenceCategoryAdvancedSettings, model.PreferenceNameAttachAppLogs)
require.Nil(t, err)
assert.Equal(t, "true", pref.Value)
}
func TestMobileLogsNoArgs(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.usage", resp.Text)
}
func TestMobileLogsTooManyArgs(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "on @"+th.BasicUser.Username)
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "on @"+th.BasicUser.Username+" trailing")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.usage", resp.Text)
pref, err := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, model.PreferenceCategoryAdvancedSettings, model.PreferenceNameAttachAppLogs)
require.Nil(t, err)
assert.Equal(t, "true", pref.Value)
}
func TestMobileLogsInvalidAction(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "invalid")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.usage", resp.Text)
}
func TestMobileLogsUserNotFound(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "on @nonexistentuser12345")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.user_not_found.app_error", resp.Text)
}
func TestMobileLogsNonexistentTargetAsRegularUserReturnsNoPermission(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "on @nonexistentuser12345")
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.cross_user_unavailable.app_error", resp.Text)
}
func TestMobileLogsOnSelfWithAtUsername(t *testing.T) {
th := setup(t).initBasic(t)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "on @"+th.BasicUser.Username)
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.enabled", resp.Text)
pref, err := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, model.PreferenceCategoryAdvancedSettings, model.PreferenceNameAttachAppLogs)
require.Nil(t, err)
assert.Equal(t, "true", pref.Value)
}
func TestMobileLogsDeactivatedUser(t *testing.T) {
th := setup(t).initBasic(t)
_, err := th.App.UpdateActive(th.Context, th.BasicUser2, false)
require.Nil(t, err)
cmd := &MobileLogsProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.SystemAdminUser.Id,
}, "on @"+th.BasicUser2.Username)
assert.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
assert.Equal(t, "api.command_mobile_logs.user_not_found.app_error", resp.Text)
}
+48
View File
@@ -1285,6 +1285,54 @@
"id": "api.command_me.name",
"translation": "me"
},
{
"id": "api.command_mobile_logs.cross_user_unavailable.app_error",
"translation": "Unable to change mobile log settings for that user."
},
{
"id": "api.command_mobile_logs.desc",
"translation": "Manage mobile app log attachment for yourself or another user."
},
{
"id": "api.command_mobile_logs.disabled",
"translation": "Mobile app log attachment is now disabled for {{.User}}."
},
{
"id": "api.command_mobile_logs.enabled",
"translation": "Mobile app log attachment is now enabled for {{.User}}."
},
{
"id": "api.command_mobile_logs.hint",
"translation": "[on|off|status] [@username]"
},
{
"id": "api.command_mobile_logs.name",
"translation": "mobile-logs"
},
{
"id": "api.command_mobile_logs.status_off",
"translation": "Mobile app log attachment is currently disabled for {{.User}}."
},
{
"id": "api.command_mobile_logs.status_on",
"translation": "Mobile app log attachment is currently enabled for {{.User}}."
},
{
"id": "api.command_mobile_logs.update_error.app_error",
"translation": "Failed to update mobile app log attachment setting. Please try again."
},
{
"id": "api.command_mobile_logs.usage",
"translation": "Usage: /mobile-logs [on|off|status] [@username]"
},
{
"id": "api.command_mobile_logs.user_not_found.app_error",
"translation": "Could not find user \"{{.Username}}\"."
},
{
"id": "api.command_mobile_logs.you",
"translation": "you"
},
{
"id": "api.command_msg.desc",
"translation": "Send Direct Message to a user"
+2
View File
@@ -30,6 +30,7 @@ const (
// - "join_leave"
// - "unread_scroll_position"
// - "sync_drafts"
// - "attach_app_logs"
// - "feature_enabled_markdown_preview" <- deprecated in favor of "formatting"
PreferenceCategoryAdvancedSettings = "advanced_settings"
// PreferenceCategoryFlaggedPost is used to store the user's saved posts.
@@ -77,6 +78,7 @@ const (
// PreferenceCategoryTheme has the name for the team id where theme is set.
PreferenceCategoryTheme = "theme"
PreferenceNameAttachAppLogs = "attach_app_logs"
PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads"
PreferenceNameChannelDisplayMode = "channel_display_mode"
PreferenceNameCollapseSetting = "collapse_previews"