mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-14 15:10:17 +00:00
Closes manual release-test gaps for mobile (Detox + Maestro), automating new test cases and hardening flaky/CI-only failures uncovered along the way. ### New coverage automated - **MM-T416** offline/online post-retry (Android via airplane mode; iOS ultimately marked unsupported — see below) - **MM-T1194 / MM-T1193** custom ToS accept/decline survives relaunch - **MM-T3251** status change via account UI (cross-user visibility) - **MM-T510** channel-wide/keyword mention delivery — later expanded to a full 5-channel mention matrix with exact server-side counts - **MM-T3261 (_1/_2)** Report a Problem external hand-off (browser link mode, email mode) - **MM-T2940** iOS managed-config (MDM) pre-fill/autoconnect - **MM-T3271/3272** opening the app from a push notification (synthetic notification payloads, background/closed states) - Un-skipped several tests that turned out to already pass (stale skips) ### Root-caused failing tests - **Android SQLite corruption**: traced to native Kotlin DB connections not opening in WAL mode while JSI did (post #10109) — journal-mode mismatch corrupted the store on push-triggered writes. Fixed properly with a **WAL patch** (#10117) rather than the original destructive "wipe app database" workaround, which was reverted. - **Fabric crash** (`addViewAt`/reparent) on `SlideUpPanelItem` taps — root-caused to view flattening racing sheet mounts; fixed with `collapsable={false}` (went through a revert/re-revert once CI reproduced the crash without it). - **Search race**: stale in-flight search results could overwrite a cleared search box — fixed with request-id tagging. - **iOS offline detection**: multiple iterations (AAAA sampling → Cloudflare-prefix check → final decision) landed on refusing iOS offline simulation outright, since all E2E servers are Cloudflare-fronted/anycast and the address space can't be enumerated/blocked. - **MM-T3261 config patch** intermittently silently ignored by the server — root-caused to `ReportAProblemType` being environment-managed on PR Spinwicks (config PATCH returns 200 but is silently dropped); now detected and reported as a skip with proven cause, not a failure. - **MM-T416 failed-post timing**: multiple rounds of tightening the wait bound to match the client's actual retry/backoff/timeout ceiling (ended at ~150s, derived from OkHttp/retry-interceptor math, not guessed). - **MM-T5603 upload wait**: raised to match the client's real 180s upload timeout. - **Classification banner suite**: fixed to verify config state directly instead of trusting a PATCH reply that the cloud sometimes drops. ### CI/reporting infra fixes - Dedicated Maestro steps (MM-T3261, MM-T67856_4) weren't feeding into the merged JUnit report and couldn't gate pass/fail — fixed report merging/naming so they now count. - Android Maestro logcat capture added (previously iOS-only diagnostics). - Jest retry logic no longer discards passing results from a shard killed by timeout. - Sharding raised 24→28. - Removed dead Detox Pilot/Playbooks scaffolding (unused, uncovered by CI). ### Cleanup - Comment/docs pass stripping narrative session-archaeology from long explanatory comments, keeping only load-bearing facts and CI references.
370 lines
12 KiB
TypeScript
370 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,
|
|
allowDuplicateWrites: !user,
|
|
label: 'apiCreateUser',
|
|
budgetMs: timeouts.HALF_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> => {
|
|
try {
|
|
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};
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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;
|