mattermost-mobile-upstream/detox/e2e/support/server_api/user.ts
yasser khan fca832c684
Some checks are pending
ci / lint-typecheck (push) Waiting to run
ci / detox-lint-typecheck (push) Waiting to run
ci / test (push) Waiting to run
fix(e2e): stabilize CMT and main Detox/Maestro E2E flakes (#10301)
* fix(e2e): preserve iOS App Group container metadata across shard specs and use swipe helper in Edit Server spec

* fix(maestro): dismiss iOS QuickPath keyboard onboarding and retry empty server URL input

* fix(e2e): avoid replaying single-use FormData streams on HTML interstitials and add ABAC/login transport retries

* fix(e2e): classify disabled-feature and server-timeout errors as infra in CMT notifications

* fix(e2e): address CodeRabbit review on container cache, ABAC cleanup and Maestro connect scroll

* fix(e2e): wait for restored denied state in upload permission spec as well

* fix(e2e): confirm restored ABAC decision server-side and scroll to URL label before Maestro recovery

* fix(e2e): return to channel list before ABAC cleanup logout and drop optional flag on recovery scroll

* fix(e2e): retry ABAC decision login on transport failures and classify CF 521/523 as transport failures

* test(e2e): cover apiUploadFileToChannel rebuilding FormData and stream on interstitial retry

* fix(e2e): budget ABAC decision waits for observed server flip latency

Co-authored-by: yasser khan <attitude3cena.yf@gmail.com>
2026-11-04 13:07:51 +05:30

382 lines
12 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {adminPassword, adminUsername} from '@support/test_config';
import {getRandomId, timeouts} from '@support/utils';
import {withTransportRetry} from '@support/utils/transport_retry';
import {logError} from '../../../provision/log';
import client from './client';
import {getResponseFromError} from './common';
/**
* Login to Mattermost server as sysadmin.
* @param {string} baseUrl - the base server URL
* @return {Object} returns {user, status} on success or {error, status} on error
*/
export const apiAdminLogin = (baseUrl: string): any => {
return apiLogin(baseUrl, {
username: adminUsername,
password: adminPassword,
});
};
/**
* Create a user.
* See https://api.mattermost.com/#operation/CreateUser
* @param {string} baseUrl - the base server URL
* @param {string} option.prefix - prefix to email and username
* @param {Object} option.user - user object to be created
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiCreateUser = async (baseUrl: string, {prefix = 'user', user = null}: any = {}): Promise<any> => {
return withTransportRetry(async () => {
try {
const newUser = user || generateRandomUser({prefix});
const response = await client.post(
`${baseUrl}/api/v4/users`,
newUser,
);
return {user: {...response.data, password: newUser.password, newUser}};
} catch (err) {
return getResponseFromError(err);
}
}, {
idempotent: false,
// Random users are safe to recreate; a caller-supplied body is not.
allowDuplicateWrites: !user,
label: 'apiCreateUser',
// Axios times out at 45s, so HALF_MIN could not fit one stall plus a retry.
budgetMs: timeouts.ONE_MIN,
});
};
/**
* Deactivate a user account.
* See https://api.mattermost.com/#operation/DeleteUser
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiDeactivateUser = async (baseUrl: string, userId: string): Promise<any> => {
try {
const response = await client.delete(`${baseUrl}/api/v4/users/${userId}`);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Demote a user to a guest.
* See https://api.mattermost.com/#operation/DemoteUserToGuest
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiDemoteUserToGuest = async (baseUrl: string, userId: string): Promise<any> => {
try {
const response = await client.post(`${baseUrl}/api/v4/users/${userId}/demote`);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get user from a current session.
* @param {string} baseUrl - the base server URL
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiGetMe = (baseUrl: string): any => {
return apiGetUserById(baseUrl, 'me');
};
/**
* Get a user by ID.
* See https://api.mattermost.com/#operation/GetUser
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiGetUserById = async (baseUrl: string, userId: string): Promise<any> => {
try {
const response = await client.get(`${baseUrl}/api/v4/users/${userId}`);
return {user: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get a user by username.
* See https://api.mattermost.com/#operation/GetUserByUsername
* @param {string} baseUrl - the base server URL
* @param {string} username - the username
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiGetUserByUsername = async (baseUrl: string, username: string): Promise<any> => {
try {
const response = await client.get(`${baseUrl}/api/v4/users/username/${username}`);
return {user: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Login to Mattermost server.
* See https://api.mattermost.com/#operation/Login
* @param {string} baseUrl - the base server URL
* @param {string} user.username - username of a user
* @param {string} user.password - password of a user
* @param {string} user.mfaToken - MFA token (required when MFA is enabled for the user)
* @return {Object} returns {user, status} on success or {error, status} on error
*/
export const apiLogin = async (baseUrl: string, user: any): Promise<any> => {
if (!user?.username || !user?.password) {
logError(
'[apiLogin] refusing to log in with incomplete credentials ' +
`(hasUsername=${Boolean(user?.username)}, hasPassword=${Boolean(user?.password)}). ` +
'Pass the user returned by apiCreateUser/apiInit, or its .newUser.',
);
return {error: {message: 'apiLogin called with incomplete credentials'}, status: 0};
}
// Password-only logins can be replayed safely on a dropped connection; MFA tokens are single-use.
return withTransportRetry(async () => {
try {
const response = await client.post(
`${baseUrl}/api/v4/users/login`,
{login_id: user.username, password: user.password, ...(user.mfaToken ? {token: user.mfaToken} : {})},
);
const {data, status} = response;
return {
status,
user: data,
};
} catch (err) {
return getResponseFromError(err);
}
}, {
idempotent: false,
allowDuplicateWrites: !user.mfaToken,
label: 'apiLogin',
budgetMs: timeouts.ONE_MIN,
});
};
/**
* Logout from the Mattermost server.
* See https://api.mattermost.com/#operation/Logout
* @param {string} baseUrl - the base server URL
* @return {Object} returns {status} on success
*/
export const apiLogout = async (baseUrl: string): Promise<any> => {
const response = await client.post(`${baseUrl}/api/v4/users/logout`);
return {status: response.status};
};
/**
* Patch user from a current session.
* @param {string} baseUrl - the base server URL
* @param {Object} userData - data to partially update a user
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiPatchMe = (baseUrl: string, userData: string): any => {
return apiPatchUser(baseUrl, 'me', userData);
};
/**
* Patch a user.
* See https://api.mattermost.com/#operation/PatchUser
* Note: UserPatch does not include roles; use apiUpdateUserRoles to change roles.
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {Object} userData - data to partially update a user
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiPatchUser = async (baseUrl: string, userId: string, userData: any): Promise<any> => {
try {
const response = await client.put(
`${baseUrl}/api/v4/users/${userId}/patch`,
userData,
);
return {user: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Update a user's roles.
* See https://api.mattermost.com/#operation/UpdateUserRoles
* Roles must be a space-separated string (e.g. 'system_user system_admin').
* Caller must have PermissionManageRoles (e.g. system admin).
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {string} roles - space-separated role names
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiUpdateUserRoles = async (baseUrl: string, userId: string, roles: string): Promise<any> => {
try {
const response = await client.put(
`${baseUrl}/api/v4/users/${userId}/roles`,
{roles},
);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Update user active status.
* See https://api.mattermost.com/#operation/UpdateUserActive
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {boolean} active - use true to set the user active, false for inactive
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiUpdateUserActiveStatus = async (baseUrl: string, userId: string, active: boolean): Promise<any> => {
try {
const response = await client.put(
`${baseUrl}/api/v4/users/${userId}/active`,
{active},
);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
type AutocompleteUser = {id: string};
type AutocompleteUsersResult = {
users?: AutocompleteUser[];
out_of_channel?: AutocompleteUser[];
error?: {message?: string};
status?: number;
};
/**
* Autocomplete users in a team/channel.
* See https://api.mattermost.com/#operation/AutocompleteUsers
* @param {string} baseUrl - the base server URL
* @param {string} option.teamId - team ID
* @param {string} option.channelId - channel ID (populates out_of_channel)
* @param {string} option.name - name prefix to match
* @return {Object} returns autocomplete payload on success or {error, status} on error
*/
export const apiAutocompleteUsers = async (
baseUrl: string,
{teamId, channelId, name}: {teamId: string; channelId?: string; name: string},
): Promise<AutocompleteUsersResult> => {
try {
const params = new URLSearchParams({in_team: teamId, name});
if (channelId) {
params.set('in_channel', channelId);
}
const response = await client.get(`${baseUrl}/api/v4/users/autocomplete?${params.toString()}`);
return response.data;
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Wait until a user appears in channel autocomplete out_of_channel results.
* Fresh users can miss the first search until the server index catches up.
*/
export const waitForUserInAutocomplete = async (
baseUrl: string,
{
teamId,
channelId,
userId,
name,
timeoutMs = 30000,
intervalMs = 1000,
}: {
teamId: string;
channelId: string;
userId: string;
name: string;
timeoutMs?: number;
intervalMs?: number;
},
): Promise<void> => {
const deadline = Date.now() + timeoutMs;
let lastError = '';
const poll = async (): Promise<void> => {
const result = await apiAutocompleteUsers(baseUrl, {teamId, channelId, name});
if ((result.out_of_channel ?? []).some((user) => user.id === userId)) {
return;
}
lastError = result.error?.message || `HTTP ${result.status ?? 'ok'}; out_of_channel=${result.out_of_channel?.length ?? 0}`;
if (Date.now() >= deadline) {
throw new Error(`User ${userId} was not in out_of_channel autocomplete for "${name}" within ${timeoutMs}ms (${lastError}).`);
}
await new Promise<void>((resolve) => {
setTimeout(resolve, intervalMs);
});
await poll();
};
await poll();
};
export const generateRandomUser = ({prefix = 'user', randomIdLength = 6} = {}) => {
const randomId = getRandomId(randomIdLength);
return {
email: `${prefix}${randomId}@sample.mattermost.com`,
username: `${prefix}${randomId}`,
// 16-char password satisfies FIPS minimum length on cloud test servers.
password: `P${randomId}!Test1234`,
first_name: `F${randomId}`,
last_name: `L${randomId}`,
nickname: `N${randomId}`,
position: `P${randomId}`,
};
};
export const User = {
apiAdminLogin,
apiAutocompleteUsers,
apiCreateUser,
apiDeactivateUser,
apiDemoteUserToGuest,
apiGetMe,
apiGetUserById,
apiGetUserByUsername,
apiLogin,
apiLogout,
apiPatchMe,
apiPatchUser,
apiUpdateUserActiveStatus,
apiUpdateUserRoles,
generateRandomUser,
waitForUserInAutocomplete,
};
export default User;