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

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

128
e2e-tests/.ci/.e2erc Normal file
View File

@@ -0,0 +1,128 @@
# shellcheck disable=SC2148,SC2155
# Function definitions
mme2e_log () { echo "[$(date +%Y-%m-%dT%H:%M:%S%Z)]" "$@"; }
mme2e_get_current_shopt_arg () {
# This function lets you get the current value of shell options, e.g. pipefail or allexport, in the form
# of arguments to pass to the 'set' shell function, in order to restore its value later
SHOPT_ARG=${1:?}
case $(set -o | sed -n -E "s/^${SHOPT_ARG}[[:space:]]+(o..?)$/\1/p") in
on) echo -n "-o ${SHOPT_ARG}";;
off) echo -n "+o ${SHOPT_ARG}";;
*) return 1;;
esac
}
mme2e_load_env_file () {
# This loads the ./env file. Variables are automatically exported
[ -f ./env ] || return 0
MME2E_PREVIOUS_ALLEXPORT=$(mme2e_get_current_shopt_arg allexport)
set -o allexport
mme2e_log "Loading env file"
. ./env
# shellcheck disable=SC2086
set ${MME2E_PREVIOUS_ALLEXPORT}
}
mme2e_generate_envfile_from_var_names () {
# Read var names from stdin, one per line
while read -r VARIABLE; do
[ -z "${!VARIABLE:-}" ] || echo "${VARIABLE}=${!VARIABLE}";
done
}
mme2e_wait_command_success () {
COMMAND=${1?}
RETRY_MESSAGE=${2?}
RETRIES_LEFT=${3:-1}
RETRIES_INTERVAL=${4:-10}
MME2E_PREVIOUS_PIPEFAIL=$(mme2e_get_current_shopt_arg pipefail)
set -o pipefail
until bash -c "${COMMAND}"; do
RETRIES_LEFT=$((RETRIES_LEFT - 1))
[ "$RETRIES_LEFT" -le "0" ] && break
mme2e_log "${RETRY_MESSAGE} ($RETRIES_LEFT retries left, sleeping $RETRIES_INTERVAL seconds)"
sleep "$RETRIES_INTERVAL"
done
# shellcheck disable=SC2086
set ${MME2E_PREVIOUS_PIPEFAIL}
if [ "$RETRIES_LEFT" = "0" ]; then
return 1
fi
}
mme2e_wait_service_healthy () {
DC_COMMAND="${DC_COMMAND:-$MME2E_DC_SERVER}"
SERVICE_NAME=${1?}
RETRIES_LEFT=${2:-1}
RETRIES_INTERVAL=${3:-10}
mme2e_wait_command_success "${DC_COMMAND} ps ${SERVICE_NAME} | grep -qE '\(healthy\)'" "Waiting for ${SERVICE_NAME} container to be healthy" "$RETRIES_LEFT" "$RETRIES_INTERVAL"
}
mme2e_wait_image () {
IMAGE_NAME=${1?}
RETRIES_LEFT=${2:-1}
RETRIES_INTERVAL=${3:-10}
mme2e_wait_command_success "docker pull --platform linux/amd64 $IMAGE_NAME" "Waiting for docker image ${IMAGE_NAME} to be available" "$RETRIES_LEFT" "$RETRIES_INTERVAL"
}
mme2e_is_token_in_list() {
local TOKEN=$1
local LIST=$2
grep -qE "(^| )$TOKEN( |$)" <<<"$LIST"
}
# Call prerequisite utility functions
mme2e_load_env_file
# Utility variables
# NB: these assume you `source` them from the directory this file is in
# Set up base docker compose file and export
export MME2E_DC_SERVER="docker compose -p mmserver -f ./server.yml"
export MME2E_DC_DASHBOARD="docker compose -p mmdashboard -f ./dashboard/docker/docker-compose.yml -f ./dashboard.override.yml"
export MME2E_UID=$(id -u)
export MME2E_OSTYPE=$(docker version -f '{{ .Client.Os }}')
export MME2E_ARCHTYPE=$(docker version -f '{{ .Client.Arch }}')
export NODE_VERSION_REQUIRED=$(cat ../../.nvmrc)
# Utility alias, for interactive shell usage. Can be reversed with 'unalias docker-compose-mmserver' in your shell
# NB: this only works in interactive shells
alias docker-compose-mmserver='${MME2E_DC_SERVER}'
alias docker-compose-mmdashboard='${MME2E_DC_DASHBOARD}'
# Default values for optional variables
export SERVER_IMAGE_DEFAULT="mattermostdevelopment/mattermost-enterprise-edition:$(git rev-parse --short=7 HEAD)"
export BROWSER_DEFAULT="chrome"
export SERVER_DEFAULT="onprem"
export TEST_DEFAULT="cypress"
export ENABLED_DOCKER_SERVICES_DEFAULT="postgres inbucket"
export BRANCH_DEFAULT=$(git branch --show-current || echo -n "unknown")
export BUILD_ID_DEFAULT=$(date +%s)
export CI_BASE_URL_DEFAULT="localhost"
# Test tool default overrides
case "${TEST:-$TEST_DEFAULT}" in
cypress )
export TEST_FILTER_DEFAULT='--stage=@prod --group=@smoke' ;;
playwright )
export TEST_FILTER_DEFAULT='--grep @smoke' ;;
* )
export TEST_FILTER_DEFAULT='' ;;
esac
# OS specific defaults overrides
case $MME2E_OSTYPE in
linux )
export IS_LINUX="-linux"
;;
darwin )
export BROWSER_DEFAULT="chrome"
export IS_LINUX=""
;;
* )
esac
# Populate the optional variables that are used in the docker-compose file
export SERVER_IMAGE=${SERVER_IMAGE:-$SERVER_IMAGE_DEFAULT}
export BROWSER=${BROWSER:-$BROWSER_DEFAULT}
export SERVER=${SERVER:-$SERVER_DEFAULT}
export TEST=${TEST:-$TEST_DEFAULT}
export ENABLED_DOCKER_SERVICES=${ENABLED_DOCKER_SERVICES:-$ENABLED_DOCKER_SERVICES_DEFAULT}
export TEST_FILTER=${TEST_FILTER:-$TEST_FILTER_DEFAULT}
export BRANCH_UNSANITIZED=${BRANCH:-$BRANCH_DEFAULT}
export BRANCH=${BRANCH_UNSANITIZED::50} # The automation dashboard only accepts branch names up to 50 characters
export BUILD_ID_UNSANITIZED=${BUILD_ID:-$BUILD_ID_DEFAULT}
export BUILD_ID=${BUILD_ID_UNSANITIZED::64} # The automation dashboard only accepts build IDs up to 64 characters
export CI_BASE_URL=${CI_BASE_URL:-$CI_BASE_URL_DEFAULT}

4
e2e-tests/.ci/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
dashboard
server.yml
env
.env.*

View File

@@ -0,0 +1,4 @@
# SC1091: Not following: <file>
# https://www.shellcheck.net/wiki/SC1091
disable=SC1091
# Used to ignore for sourcing ".e2erc*"

View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -e -u -o pipefail
npm install
# Run migrations. This is also a way to wait for the database to be up
MIGRATION_ATTEMPTS_LEFT=10
MIGRATION_ATTEMPTS_INTERVAL=10
until npm run migrate:latest; do
MIGRATION_ATTEMPTS_LEFT=$((MIGRATION_ATTEMPTS_LEFT - 1))
[ "$MIGRATION_ATTEMPTS_LEFT" -gt 0 ] || break
echo "Migration script failed, sleeping $MIGRATION_ATTEMPTS_INTERVAL"
sleep $MIGRATION_ATTEMPTS_INTERVAL
done
[ "$MIGRATION_ATTEMPTS_LEFT" -gt 0 ] || {
echo "Migration script failed, exhausted attempts. Giving up."
exit 1
}
# Launch the dashboard
exec npm run dev

View File

@@ -0,0 +1,26 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
TEST_ENV_FILE=".env.$TEST"
mme2e_log "Loading variables from $TEST_ENV_FILE"
if ! [ -f "$TEST_ENV_FILE" ]; then
mme2e_log "Error: $TEST_ENV_FILE is required to exist, for the test cycle to be generated. Aborting." >&2
exit 1
fi
set -a
# shellcheck disable=SC1090
. "$TEST_ENV_FILE"
if [ -z "${AUTOMATION_DASHBOARD_URL:-}" ]; then
mme2e_log "AUTOMATION_DASHBOARD_URL is unset. Skipping test cycle generation."
exit 0
fi
mme2e_log "Generating the test cycle on the Automation Dashboard"
cd "../$TEST"
npm i
# shellcheck disable=SC2086
exec node --trace-warnings generate_test_cycle.js $TEST_FILTER

View File

@@ -0,0 +1,25 @@
---
services:
dashboard:
image: mattermostdevelopment/mirrored-node:18.17
environment:
PG_URI: postgres://mmuser:mostest_password@db:5432/automation_dashboard_db
JWT_SECRET: s8gGBA3ujKRohSw1L8HLOY7Jjnu2ZYv8 # Generated with e.g. `dd if=/dev/urandom count=24 bs=1 2>/dev/null | base64 -w0`
JWT_USER: cypress-test
JWT_ROLE: integration
JWT_ALG: HS256
JWT_EXPIRES_IN: 365d
ALLOWED_USER: cypress-test
ALLOWED_ROLE: integration
healthcheck:
test: ["CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:4000"]
interval: 10s
timeout: 15s
retries: 12
volumes:
- "../:/app"
- "../../dashboard.entrypoint.sh:/usr/local/bin/dashboard.entrypoint.sh:ro"
working_dir: /app
entrypoint: /usr/local/bin/dashboard.entrypoint.sh
ports:
- 4000:4000

View File

@@ -0,0 +1,50 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
MME2E_DASHBOARD_REF_DEFAULT="origin/main"
MME2E_DASHBOARD_REF=${MME2E_DASHBOARD_REF:-$MME2E_DASHBOARD_REF_DEFAULT}
mme2e_log "Cloning the automation-dashboard project"
if [ ! -d dashboard ]; then
git clone https://github.com/saturninoabril/automation-dashboard.git dashboard
# Must reinitialize some variables that depend on the dashboard repo being checked out
. .e2erc
fi
git -C dashboard fetch
git -C dashboard checkout "$MME2E_DASHBOARD_REF"
mme2e_log "Starting the dashboard"
${MME2E_DC_DASHBOARD} up -d db dashboard
if ! DC_COMMAND="${MME2E_DC_DASHBOARD}" mme2e_wait_service_healthy dashboard 60 10; then
mme2e_log "Dashboard container not healthy, retry attempts exhausted. Giving up." >&2
exit 1
fi
mme2e_log "Generating the dashboard's local URL"
case $MME2E_OSTYPE in
darwin)
# shellcheck disable=SC2046
AUTOMATION_DASHBOARD_IP=$(ifconfig $(route get 1.1.1.1 | grep interface | awk '{print $2}') | grep -w inet | awk '{print $2}')
;;
*)
# shellcheck disable=SC2046
MME2E_DC_DASHBOARD_NETWORK=$(docker inspect $(${MME2E_DC_DASHBOARD} ps -q dashboard) | jq -r '.[0].NetworkSettings.Networks | (keys|.[0])')
AUTOMATION_DASHBOARD_IP=$(docker network inspect "$MME2E_DC_DASHBOARD_NETWORK" | jq -r '.[0].IPAM.Config[0].Gateway')
;;
esac
# shellcheck disable=SC2034
AUTOMATION_DASHBOARD_URL="http://${AUTOMATION_DASHBOARD_IP}:4000/api"
mme2e_log "Generating a signed JWT token for accessing the dashboard"
# Not running 'npm install'. We assume it has been run by the dashboard container's entrypoint
# shellcheck disable=SC2034
AUTOMATION_DASHBOARD_TOKEN=$(${MME2E_DC_DASHBOARD} exec -T dashboard node script/sign.js | awk '{ print $2; }') # The token secret is specified in the dashboard.override.yml file
mme2e_log "Generating the .env.dashboard file, to point Cypress to the dashboard URL"
mme2e_generate_envfile_from_var_names >.env.dashboard <<EOF
AUTOMATION_DASHBOARD_URL
AUTOMATION_DASHBOARD_TOKEN
EOF

12
e2e-tests/.ci/dashboard.stop.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
if [ -d dashboard ]; then
mme2e_log "Stopping the dashboard containers"
${MME2E_DC_DASHBOARD} down
else
# Base docker-compose file is found in the dashboard repo, we can't run docker commands without it
mme2e_log "Not stopping the dashboard containers: dashboard repo not checked out locally, skipping"
fi

27
e2e-tests/.ci/report.collect.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# SC2034: <variable> appears unused.
# https://www.shellcheck.net/wiki/SC2034
# shellcheck disable=SC2034
# shellcheck disable=SC2086,SC2223
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
# The collected data will be written to the "e2e-tests/$TEST/results/" directory
cd "../$TEST/"
if [ ! -d "results/" ]; then
mme2e_log "Error: 'results/' directory does not exist. Aborting report data collection." >&2
exit 1
fi
# If the Automation Dashboard is used, try to collect run data from it
if [ -n "${AUTOMATION_DASHBOARD_URL:-}" ]; then
mme2e_log "Automation Dashboard usage detected: retrieving cycle results"
# Assume that we're in the cypress dir and that the 'results/' directory exists
: ${BUILD_ID:?}
AD_CYCLE_FILE="results/ad_cycle.json"
AD_SPECS_FILE="results/ad_specs.json"
curl -o "$AD_CYCLE_FILE" -fsSL "${AUTOMATION_DASHBOARD_URL}/cycle?build=${BUILD_ID}"
curl -o "$AD_SPECS_FILE" -fsSL "${AUTOMATION_DASHBOARD_URL}/executions/specs?cycle_id=$(jq -r .id "$AD_CYCLE_FILE")"
fi

115
e2e-tests/.ci/report.publish.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/bin/bash
# SC2034: <variable> appears unused.
# https://www.shellcheck.net/wiki/SC2034
# shellcheck disable=SC2034
# shellcheck disable=SC2086,SC2223
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
# Default or assert variables required by the save_report.js script, and document optional variables
: ${FULL_REPORT:=false} # Valid values: true, false
: ${TYPE:=NONE} # Valid values: PR, RELEASE, MASTER, MASTER_UNSTABLE, CLOUD, CLOUD_UNSTABLE, NONE (which is the same as omitting it); also known as REPORT_TYPE
: ${WEBHOOK_URL:-} # Optional. Mattermost webhook to post the report back to
: ${RELEASE_DATE:-} # Optional. If set, its value will be included in the report as the release date of the tested artifact
if [ "$TYPE" = "PR" ]; then
# Try to determine PR number: first from PR_NUMBER, then from BRANCH (server-pr-XXXX format)
if [ -n "${PR_NUMBER:-}" ]; then
export PULL_REQUEST="https://github.com/mattermost/mattermost/pull/${PR_NUMBER}"
elif grep -qE '^server-pr-[0-9]+$' <<<"${BRANCH:-}"; then
PR_NUMBER="${BRANCH##*-}"
export PULL_REQUEST="https://github.com/mattermost/mattermost/pull/${PR_NUMBER}"
else
mme2e_log "Warning: TYPE=PR but cannot determine PR number from PR_NUMBER or BRANCH. Falling back to TYPE=NONE."
TYPE=NONE
fi
fi
# Env vars used during the test. Their values will be included in the report
: ${TEST:?} # See E2E tests' readme
: ${BRANCH:?} # May be either a ref, a commit hash, or 'server-pr-PR_NUMBER' (if TYPE=PR)
: ${BUILD_ID:?}
: ${SERVER:?} # May be either 'onprem' or 'cloud'
: ${MM_ENV:-}
# Populate intermediate variables
export BUILD_TAG="${SERVER_IMAGE##*/}"
export MM_DOCKER_IMAGE="${BUILD_TAG%%:*}" # NB: the 'mattermostdevelopment/' prefix is assumed
export MM_DOCKER_TAG="${BUILD_TAG##*:}"
export SERVER_TYPE="${SERVER}"
export AUTOMATION_DASHBOARD_FRONTEND_URL="${AUTOMATION_DASHBOARD_URL:+${AUTOMATION_DASHBOARD_URL%%/api}}"
if [ -n "${TM4J_API_KEY:-}" ]; then
# Set additional variables required for Zephyr reporting
if grep -q 'UNSTABLE' <<<"$TYPE"; then
export TEST_SUITE=prod
else
export TEST_SUITE=unstable
fi
if [ "$TYPE" = "RELEASE" ]; then
export SERVER_ARTIFACT_TYPE=release
else
export SERVER_ARTIFACT_TYPE=master
export TEST_IS_DAILY=yes
fi
export TM4J_ENABLE=true
export JIRA_PROJECT_KEY=MM
export TM4J_ENVIRONMENT_NAME="${TEST@u}/${BROWSER@u}/${SERVER_ARTIFACT_TYPE}-${SERVER_TYPE}-ent${TEST_IS_DAILY:+-${TEST_SUITE}}"
# Assert that the test type is among the ones supported by Zephyr, and select the corresponding folderId
case "${SERVER}-${TYPE}" in
onprem-RELEASE)
export TM4J_FOLDER_ID="2014475"
;;
onprem-MASTER)
export TM4J_FOLDER_ID="2014476"
;;
onprem-MASTER_UNSTABLE)
export TM4J_FOLDER_ID="2014478"
;;
cloud-RELEASE)
export TM4J_FOLDER_ID="2014474"
;;
cloud-CLOUD)
export TM4J_FOLDER_ID="2014479"
;;
cloud-CLOUD_UNSTABLE)
export TM4J_FOLDER_ID="2014481"
;;
*)
mme2e_log "Error: unsupported Zephyr environment for the requested report (SERVER=${SERVER}, TYPE=${TYPE}). Aborting." >&2
exit 1
;;
esac
: ${TEST_CYCLE_LINK_PREFIX:?}
: ${TM4J_CYCLE_KEY:-} # Optional. Populated automatically by the reporting script
: ${TM4J_CYCLE_NAME:-} # Optional. Populated automatically by the reporting script
mme2e_log "TMJ4 integration enabled. Environment: $TM4J_ENVIRONMENT_NAME"
fi
if [ -n "${DIAGNOSTIC_WEBHOOK_URL:-}" ]; then
: ${DIAGNOSTIC_USER_ID:?}
: ${DIAGNOSTIC_TEAM_ID:?}
mme2e_log "Diagnostic report upload enabled."
fi
# Double check that the "results/" subdirectory to collect report informations from exists
cd "../${TEST}/"
if [ ! -d "results/" ]; then
mme2e_log "Error: 'results/' directory does not exist. Aborting report data collection." >&2
exit 1
fi
case "$TEST" in
cypress)
npm i
node save_report.js
;;
playwright)
if [ -n "$WEBHOOK_URL" ]; then
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i
# Utilize environment data and report files to generate the webhook body
node report.webhookgen.js | curl -X POST -fsSL -H 'Content-Type: application/json' -d @- "$WEBHOOK_URL"
fi
;;
esac

View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
[ -f .env.cloud ] && . .env.cloud
if [ "$SERVER" != "cloud" ]; then
mme2e_log "Skipping cloud instance initialization: operation supported only for cloud server, but running with SERVER='$SERVER'"
exit 0
elif [ -n "${MM_CUSTOMER_ID:-}" ]; then
mme2e_log "Skipping cloud user creation: customer with ID '$MM_CUSTOMER_ID' is already configured. Please run 'make cloud-teardown' before creating a new user."
exit 0
fi
mme2e_log "Initializing cloud tests"
# Assert that required variables are set
MME2E_ENVCHECK_MSG="variable required for initializing cloud tests, but is empty or unset."
: "${CWS_URL:?$MME2E_ENVCHECK_MSG}"
: "${MM_LICENSE:?$MME2E_ENVCHECK_MSG}"
response=$(curl -fsSL -X POST -H @- "${CWS_URL}/api/v1/tests/create-customer?sku=cloud-enterprise&is_paid=true" <<<"${CWS_EXTRA_HTTP_HEADERS:-}")
MM_CUSTOMER_ID=$(echo "$response" | jq -r .customer_id)
MM_CLOUD_API_KEY=$(echo "$response" | jq -r .api_key)
MM_CLOUD_INSTALLATION_ID=$(echo "$response" | jq -r .installation_id)
export MM_CUSTOMER_ID=$MM_CUSTOMER_ID
export MM_CLOUD_API_KEY=$MM_CLOUD_API_KEY
export MM_CLOUD_INSTALLATION_ID=$MM_CLOUD_INSTALLATION_ID
export MM_CLOUDSETTINGS_CWSURL=$CWS_URL
export MM_CLOUDSETTINGS_CWSAPIURL=$CWS_URL
mme2e_generate_envfile_from_var_names >.env.cloud <<EOF
MM_CLOUDSETTINGS_CWSURL
MM_CLOUDSETTINGS_CWSAPIURL
MM_CLOUD_API_KEY
MM_CUSTOMER_ID
MM_CLOUD_INSTALLATION_ID
EOF
mme2e_log "Test cloud customer created, MM_CUSTOMER_ID: $MM_CUSTOMER_ID."

View File

@@ -0,0 +1,26 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
[ -f .env.cloud ] && . .env.cloud
if [ -z "${MM_CUSTOMER_ID:-}" ]; then
mme2e_log "Skipping cloud instance teardown: MM_CUSTOMER_ID variable is empty, no cloud user to cleanup."
exit 0
fi
mme2e_log "Tearing down cloud tests"
mme2e_log "Loading .env.cloud"
. .env.cloud
# Assert that required variables are set
MME2E_ENVCHECK_MSG="variable required for tearing down cloud tests, but is empty or unset."
: "${CWS_URL:?$MME2E_ENVCHECK_MSG}"
: "${MM_CUSTOMER_ID:?$MME2E_ENVCHECK_MSG}"
mme2e_log "Deleting customer $MM_CUSTOMER_ID."
curl -fsSL -X DELETE -H @- "${CWS_URL}/api/v1/tests/customers/$MM_CUSTOMER_ID/payment-customer" <<<"${CWS_EXTRA_HTTP_HEADERS:-}"
rm -fv .env.cloud
mme2e_log "Test cloud customer deleted, MM_CUSTOMER_ID: $MM_CUSTOMER_ID."

477
e2e-tests/.ci/server.generate.sh Executable file
View File

@@ -0,0 +1,477 @@
#!/bin/bash
# SC2034: <variable> appears unused.
# https://www.shellcheck.net/wiki/SC2034
# shellcheck disable=SC2034
# Note: Variables are dynamically used depending on usage input (ENABLED_DOCKER_SERVICES)
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
enable_docker_service() {
local SERVICE_TO_ENABLE="$1"
if ! mme2e_is_token_in_list "$SERVICE_TO_ENABLE" "$ENABLED_DOCKER_SERVICES"; then
ENABLED_DOCKER_SERVICES="$ENABLED_DOCKER_SERVICES $SERVICE_TO_ENABLE"
fi
}
assert_docker_services_validity() {
local SERVICES_TO_CHECK="$*"
local SERVICES_VALID="postgres minio inbucket openldap elasticsearch opensearch redis keycloak cypress webhook-interactions playwright"
local SERVICES_REQUIRED="postgres inbucket"
for SERVICE_NAME in $SERVICES_TO_CHECK; do
if ! mme2e_is_token_in_list "$SERVICE_NAME" "$SERVICES_VALID"; then
mme2e_log "Error, requested invalid service: $SERVICE_NAME" >&2
mme2e_log "Valid services are: $SERVICES_VALID" >&2
mme2e_log "Aborting" >&2
exit 1
fi
SERVICES_REQUIRED="${SERVICES_REQUIRED/$SERVICE_NAME/}"
done
if [ -n "${SERVICES_REQUIRED/ /}" ]; then
mme2e_log "Missing required services: $SERVICES_REQUIRED" >&2
mme2e_log "Aborting" >&2
exit 2
fi
}
generate_docker_compose_file() {
# Generating the server docker-compose file
local DC_FILE="server.yml"
mme2e_log "Generating docker-compose file in: $DC_FILE"
# Generate the docker compose override file
cat <<EOL >"$DC_FILE"
# Image hashes in this file are for amd64 systems
# NB: May include paths relative to the "server/build" directory, which contains the original compose file that this yaml is overriding
services:
server:
image: \${SERVER_IMAGE}
platform: linux/amd64
restart: always
env_file:
- "./.env.server"
environment:
MM_SERVICESETTINGS_ALLOWCORSFROM: "*"
MM_SERVICESETTINGS_ENABLELOCALMODE: "true"
MM_SERVICESETTINGS_ENABLESECURITYFIXALERT: "false"
MM_CONNECTEDWORKSPACESSETTINGS_ENABLEREMOTECLUSTERSERVICE: "true"
MM_CONNECTEDWORKSPACESSETTINGS_ENABLESHAREDWORKSPACES: "true"
MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:mostest_password@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10&binary_parameters=yes"
MM_SQLSETTINGS_DRIVERNAME: "postgres"
MM_EMAILSETTINGS_SMTPSERVER: "localhost"
MM_CLUSTERSETTINGS_READONLYCONFIG: "false"
MM_SERVICEENVIRONMENT: "test"
MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES: "true"
MM_FEATUREFLAGS_PERMISSIONPOLICIES: "true"
MM_FEATUREFLAGS_TEAMMEMBERSHIPACCESSCONTROL: "true"
MM_FEATUREFLAGS_CLASSIFICATIONMARKINGS: "true"
MM_FEATUREFLAGS_INTEGRATEDBOARDS: "true"
MM_FEATUREFLAGS_PROPERTYFIELDRANK: "true"
MM_FEATUREFLAGS_RESOURCEATTRIBUTESINPOLICIES: "true"
MM_FEATUREFLAGS_ATTRIBUTEVALUEMASKING: "true"
MM_FEATUREFLAGS_WYSIWYGEDITOR: "true"
MM_FEATUREFLAGS_RECURRINGSCHEDULEDPOSTS: "true"
MM_FEATUREFLAGS_ENABLECONCURRENTREACT: "true"
MM_LOGSETTINGS_ENABLEDIAGNOSTICS: "false"
MM_LOGSETTINGS_CONSOLELEVEL: "DEBUG"
network_mode: host
ports:
- "8065:8065"
depends_on:
$(for service in $ENABLED_DOCKER_SERVICES; do
# The server container will start only if all other dependent services are healthy
# Skip creating the dependency for containers that don't need a healthcheck
if grep -qE "^(cypress|webhook-interactions|playwright)" <<<"$service"; then
continue
fi
echo " $service:"
echo " condition: service_healthy"
done)
$(if mme2e_is_token_in_list "postgres" "$ENABLED_DOCKER_SERVICES"; then
echo '
postgres:
image: mattermostdevelopment/mirrored-postgres:15
restart: "no"
network_mode: host
networks: !reset []
extends:
file: ../../server/build/docker-compose.common.yml
service: postgres'
fi)
$(if mme2e_is_token_in_list "inbucket" "$ENABLED_DOCKER_SERVICES"; then
echo '
inbucket:
network_mode: host
networks: !reset []
restart: "no"
extends:
file: ../../server/build/docker-compose.common.yml
service: inbucket
healthcheck:
test: [ "CMD", "nc", "-z", "-w1", "127.0.0.1", "10025" ]
interval: 10s
timeout: 15s
retries: 12'
fi)
$(if mme2e_is_token_in_list "minio" "$ENABLED_DOCKER_SERVICES"; then
echo '
minio:
network_mode: host
networks: !reset []
restart: "no"
extends:
file: ../../server/build/docker-compose.common.yml
service: minio
healthcheck:
test: [ "CMD", "mc", "ready", "local" ]
interval: 10s
timeout: 15s
retries: 12'
fi)
$(if mme2e_is_token_in_list "openldap" "$ENABLED_DOCKER_SERVICES"; then
echo '
openldap:
network_mode: host
networks: !reset []
restart: "no"
extends:
file: ../../server/build/docker-compose.common.yml
service: openldap
healthcheck:
test: [ "CMD", "bash", "-o", "pipefail", "-c", "ss -ltn \"sport = :636\" | grep -qE \"^LISTEN\"" ]
interval: 10s
timeout: 15s
retries: 12'
fi)
$(if mme2e_is_token_in_list "elasticsearch" "$ENABLED_DOCKER_SERVICES"; then
echo '
elasticsearch:
restart: "no"
network_mode: host
networks: !reset []
environment:
xpack.security.enabled: "false"
action.destructive_requires_name: "false"
extends:
file: ../../server/build/docker-compose.common.yml
service: elasticsearch
healthcheck:
test: [ "CMD", "curl", "-fsS", "localhost:9200/_cluster/health?wait_for_status=green&timeout=5s" ]
interval: 10s
timeout: 15s
retries: 12'
if [ "$MME2E_ARCHTYPE" = "arm64" ]; then
echo '
image: mattermostdevelopment/mattermost-elasticsearch:8.9.0
platform: linux/arm64/v8'
fi
fi)
$(if mme2e_is_token_in_list "opensearch" "$ENABLED_DOCKER_SERVICES"; then
echo '
opensearch:
restart: "no"
network_mode: host
networks: !reset []
environment:
http.port: 9200
extends:
file: ../../server/build/docker-compose.common.yml
service: opensearch
healthcheck:
test: [ "CMD", "curl", "-fsS", "localhost:9200/_cluster/health?wait_for_status=green&timeout=5s" ]
interval: 10s
timeout: 15s
retries: 12'
fi)
$(if mme2e_is_token_in_list "redis" "$ENABLED_DOCKER_SERVICES"; then
echo '
redis:
restart: "no"
network_mode: host
networks: !reset []
extends:
file: ../../server/build/docker-compose.common.yml
service: redis
healthcheck:
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
interval: 10s
timeout: 15s
retries: 12'
fi)
$(if mme2e_is_token_in_list "keycloak" "$ENABLED_DOCKER_SERVICES"; then
echo '
keycloak:
restart: "no"
network_mode: host
networks: !reset []
environment:
JAVA_OPTS: "-Xms64m -Xmx2G -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true"
volumes:
- "../../server/build/docker/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json"
- "../../server/build/docker/keycloak/kc-healthcheck.sh:/usr/local/bin/kc-healthcheck.sh"
extends:
file: ../../server/build/docker-compose.common.yml
service: keycloak
healthcheck:
# We cannot use a simple curl --silent localhost:9990/health | grep -q \"status\":\"UP\" because theres no curl in the image: https://www.keycloak.org/server/health#_using_the_health_checks
test: [ "CMD", "/usr/local/bin/kc-healthcheck.sh" ]
interval: 10s
timeout: 15s
retries: 12'
fi)
$(if mme2e_is_token_in_list "cypress" "$ENABLED_DOCKER_SERVICES"; then
echo '
cypress:
image: "cypress/browsers:node-24.14.0-chrome-145.0.7632.116-1-ff-148.0-edge-145.0.3800.70-1"
platform: linux/amd64
entrypoint: ["/bin/bash", "-c"]
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
env_file:
- "../../e2e-tests/.ci/.env.cypress"
environment:
CYPRESS_baseUrl: "http://localhost:8065"
CYPRESS_dbConnection: "postgres://mmuser:mostest_password@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10"
CYPRESS_smtpUrl: "http://localhost:9001"
CYPRESS_webhookBaseUrl: "http://localhost:3000"
CYPRESS_chromeWebSecurity: "false"
CYPRESS_firstTest: "true"
CYPRESS_resetBeforeTest: "true"
CYPRESS_allowedUntrustedInternalConnections: "localhost webhook-interactions"
TM4J_ENABLE: "false"
# disable shared memory X11 affecting Cypress v4 and Chrome
# https://github.com/cypress-io/cypress-docker-images/issues/270
QT_X11_NO_MITSHM: "1"
_X11_NO_MITSHM: "1"
_MITSHM: "0"
# avoid too many progress messages
# https://github.com/cypress-io/cypress/issues/1243
CI: "1"
# Use /tmp for Cypress cache and config so any UID can write without permission issues
CYPRESS_CACHE_FOLDER: /tmp/cypress-cache
XDG_CONFIG_HOME: /tmp/xdg-config
# Ensure we are independent from the global node environment
PATH: /e2e-tests/cypress/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ulimits:
nofile:
soft: 8096
hard: 1048576
working_dir: /e2e-tests/cypress
network_mode: host
volumes:
- "../../e2e-tests/cypress/:/e2e-tests/cypress"
- "../../webapp/platform/eslint-plugin/:/webapp/platform/eslint-plugin"'
fi)
$(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; then
# shellcheck disable=SC2016
echo '
webhook-interactions:
image: node:${NODE_VERSION_REQUIRED}
command: sh -c "npm init -y > /dev/null && npm install express@5.1.0 axios@1.11.0 client-oauth2@github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49 && exec node webhook_serve.js"
healthcheck:
test: ["CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:3000"]
interval: 10s
timeout: 15s
retries: 12
working_dir: /webhook
network_mode: host
restart: on-failure
volumes:
- "../../e2e-tests/cypress/webhook_serve.js:/webhook/webhook_serve.js:ro"
- "../../e2e-tests/cypress/utils/:/webhook/utils:ro"
- "../../e2e-tests/cypress/tests/plugins/post_message_as.js:/webhook/tests/plugins/post_message_as.js:ro"'
fi)
$(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
# shellcheck disable=SC2016
echo '
playwright:
image: mcr.microsoft.com/playwright:v1.62.0-noble
entrypoint: ["/bin/bash", "-c"]
command:
- |
# Install Node.js based on .nvmrc
NODE_VERSION=$$(cat /mattermost/.nvmrc)
echo "Installing Node.js $${NODE_VERSION}..."
curl -fsSL https://deb.nodesource.com/setup_$${NODE_VERSION%%.*}.x | bash -
apt-get install -y nodejs
echo "Node.js version: $$(node --version)"
# Wait for termination signal
until [ -f /var/run/mm_terminate ]; do sleep 5; done
env_file:
- "./.env.playwright"
environment:
CI: "true"
PLAYWRIGHT_SKIP_BROWSER_GC: 1
PW_BASE_URL: http://localhost:8065
PW_ADMIN_USERNAME: sysadmin
PW_ADMIN_PASSWORD: Sys@dmin-sample1
PW_ADMIN_EMAIL: sysadmin@sample.mattermost.com
PW_ENSURE_PLUGINS_INSTALLED: ""
MM_TEST_DB_URL: "postgres://mmuser:mostest_password@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10&binary_parameters=yes"
PW_HA_CLUSTER_ENABLED: "false"
PW_RESET_BEFORE_TEST: "false"
PW_HEADLESS: "true"
PW_SLOWMO: 0
PW_WORKERS: 1
PW_SNAPSHOT_ENABLE: "false"
PW_PERCY_ENABLE: "false"
PW_WEBHOOK_BASE_URL: http://localhost:3000
ulimits:
nofile:
soft: 8096
hard: 1048576
working_dir: /mattermost
network_mode: host
volumes:
- "../../:/mattermost"'
fi)
EOL
mme2e_log "docker-compose file generated."
}
generate_env_files() {
# Generate .env.server
mme2e_log "Generating .env.server"
truncate -s 0 .env.server
# Setting SERVER-specific variables
case "$SERVER" in
cloud)
echo "MM_NOTIFY_ADMIN_COOL_OFF_DAYS=0.00000001" >>.env.server
echo 'MM_FEATUREFLAGS_ANNUALSUBSCRIPTION="true"' >>.env.server
# Load .env.cloud into .env.server
# We assume the file exist at this point. The actual check for that should be done before calling this function
cat >>.env.server <.env.cloud
;;
esac
# Setting MM_ENV-injected variables
# shellcheck disable=SC2086
envarr=$(echo ${MM_ENV:-} | tr "," "\n")
for env in $envarr; do
echo "$env" >>.env.server
done
# Generating service-specific env vars
for SERVICE in $ENABLED_DOCKER_SERVICES; do
case $SERVICE in
opensearch)
echo "MM_ELASTICSEARCHSETTINGS_BACKEND=opensearch" >>.env.server
;;
esac
done
# Generating TEST-specific env files
# Some are defaulted in .e2erc due to being needed to other scripts as well
export REPO=mattermost # Static, but declared here for making generate_test_cycle.js easier to run
export HEADLESS=true # Static, but declared here for making generate_test_cycle.js easier to run
case "$TEST" in
cypress)
mme2e_log "Cypress: Generating .env.cypress"
truncate -s 0 .env.cypress
mme2e_generate_envfile_from_var_names >.env.cypress <<-EOF
BRANCH
BUILD_ID
CI_BASE_URL
BROWSER
HEADLESS
REPO
CYPRESS_pushNotificationServer
EOF
# Adding service-specific cypress variables
for SERVICE in $ENABLED_DOCKER_SERVICES; do
case $SERVICE in
openldap)
echo "CYPRESS_ldapServer=localhost" >>.env.cypress
echo "CYPRESS_runLDAPSync=true" >>.env.cypress
;;
minio)
echo "CYPRESS_minioS3Endpoint=localhost:9000" >>.env.cypress
;;
keycloak)
echo "CYPRESS_keycloakBaseUrl=http://localhost:8484" >>.env.cypress
;;
elasticsearch | opensearch)
echo "CYPRESS_elasticsearchConnectionURL=http://localhost:9200" >>.env.cypress
;;
esac
done
# Adding SERVER-specific cypress variables
case "$SERVER" in
cloud)
echo "CYPRESS_serverEdition=Cloud" >>.env.cypress
echo "CYPRESS_cwsURL=${CWS_URL}" >>.env.cypress
echo "CYPRESS_cwsAPIURL=${CWS_URL}" >>.env.cypress
;;
*)
echo "CYPRESS_serverEdition=E20" >>.env.cypress
;;
esac
# Add Automation Dashboard related variables to cypress container
if [ -n "${AUTOMATION_DASHBOARD_URL:-}" ]; then
mme2e_log "Automation dashboard URL is set: loading related variables into the Cypress container"
mme2e_generate_envfile_from_var_names >>.env.cypress <<-EOF
AUTOMATION_DASHBOARD_URL
AUTOMATION_DASHBOARD_TOKEN
EOF
elif DC_COMMAND="$MME2E_DC_DASHBOARD" mme2e_wait_service_healthy dashboard 1; then
mme2e_log "Detected a running automation dashboard: loading its access variables into the Cypress container"
cat >>.env.cypress <.env.dashboard
fi
;;
playwright)
mme2e_log "Playwright: Generating .env.playwright"
mme2e_generate_envfile_from_var_names >.env.playwright <<-EOF
BRANCH
BUILD_ID
EOF
;;
none)
mme2e_log "Requested TEST=$TEST. Skipping generation of test-specific env files."
;;
esac
}
# Perform SERVER-specific checks/customizations
case "$SERVER" in
cloud)
if ! [ -f .env.cloud ]; then
mme2e_log "Error: when using SERVER=$SERVER, the .env.cloud file is expected to exist, before generating the docker-compose file. Aborting." >&2
exit 1
fi
;;
esac
# Perform TEST-specific checks/customizations
case $TEST in
cypress)
enable_docker_service cypress
enable_docker_service webhook-interactions
;;
playwright)
enable_docker_service playwright
enable_docker_service webhook-interactions
;;
esac
mme2e_log "Generating docker-compose file using the following parameters..."
mme2e_log "TEST: ${TEST}"
mme2e_log "SERVER: ${SERVER}"
mme2e_log "ENABLED_DOCKER_SERVICES: ${ENABLED_DOCKER_SERVICES}"
assert_docker_services_validity "$ENABLED_DOCKER_SERVICES"
generate_docker_compose_file
generate_env_files

74
e2e-tests/.ci/server.prepare.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
mme2e_log "Configuring starting server parameters that may be changed at runtime"
for SETTING in \
TeamSettings.EnableOpenServer=true \
PluginSettings.Enable=true \
PluginSettings.EnableUploads=true \
PluginSettings.AutomaticPrepackagedPlugins=true; do
mme2e_log "Configuring parameter: $SETTING"
# shellcheck disable=SC2046
${MME2E_DC_SERVER} exec -T -- server mmctl --local config set $(tr '=' ' ' <<<$SETTING)
done
if [ -n "${MM_LICENSE:-}" ]; then
# We prefer uploading the license here, instead of setting the env var for the server
# This is to retain the flexibility of being able to remove it programmatically, if the tests require it
mme2e_log "Uploading license to server"
${MME2E_DC_SERVER} exec -T -- server mmctl --local license upload-string "$MM_LICENSE"
fi
# if [ "$TEST" = "cypress" ]; then
# mme2e_log "Prepare Cypress: install dependencies"
# # Create user in Cypress container and work around the node image's assumption that the app files are owned by user 1000
# mme2e_log "Creating user for Cypress container"
# ${MME2E_DC_SERVER} exec -T -u 0 -- cypress bash -c "id $MME2E_UID || useradd -u $MME2E_UID -m nodeci"
# ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress npm i
# ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress cypress install
# mme2e_log "Prepare Cypress: populating fixtures"
# ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress tee tests/fixtures/keycloak.crt >/dev/null <../../server/build/docker/keycloak/keycloak.crt
# # Download plugins using wget in the Cypress container
# for PLUGIN_URL in \
# "https://github.com/mattermost/mattermost-plugin-gitlab/releases/download/v1.3.0/com.github.manland.mattermost-plugin-gitlab-1.3.0.tar.gz" \
# "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.9.0/com.mattermost.demo-plugin-0.9.0.tar.gz" \
# "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.8.0/com.mattermost.demo-plugin-0.8.0.tar.gz"; do
# PLUGIN_NAME="${PLUGIN_URL##*/}"
# PLUGIN_PATH="tests/fixtures/$PLUGIN_NAME"
# # Check if file exists
# if ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress test -f "$PLUGIN_PATH"; then
# mme2e_log "Skipping installation of plugin $PLUGIN_NAME: file exists"
# continue
# fi
# # Use wget in the Cypress container instead of curl in server (for distroless compatibility)
# mme2e_log "Downloading $PLUGIN_NAME to fixtures directory"
# ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress wget -q -O "/cypress/$PLUGIN_PATH" "$PLUGIN_URL"
# done
# fi
# Run service-specific initialization steps
for SERVICE in $ENABLED_DOCKER_SERVICES; do
case "$SERVICE" in
openldap)
LDIF_FILE=../../server/tests/test-data.ldif
LDIF_CANARY=$(sed -n -E 's/^dn:[[:space:]]*(.*)$/\1/p' ${LDIF_FILE} | tail -n1)
if ${MME2E_DC_SERVER} exec -T -- openldap bash -c "ldapsearch -x -D \"cn=admin,dc=mm,dc=test,dc=com\" -w mostest_password -b \"$LDIF_CANARY\" >/dev/null"; then
mme2e_log "Skipping configuration for the $SERVICE container: already initialized"
continue
fi
mme2e_log "Configuring the $SERVICE container"
${MME2E_DC_SERVER} exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest_password || true' <../../server/tests/custom-schema-objectID.ldif
${MME2E_DC_SERVER} exec -T -- openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest_password || true' <../../server/tests/custom-schema-cpa.ldif
${MME2E_DC_SERVER} exec -T -- openldap bash -c 'ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest_password' <../../server/tests/test-data.ldif
;;
minio)
mme2e_log "Configuring the $SERVICE container"
${MME2E_DC_SERVER} exec -T -- minio sh -c 'mkdir -p /data/mattermost-test'
;;
esac
done
mme2e_log "Mattermost is running and ready for E2E testing"

View File

@@ -0,0 +1,75 @@
#!/bin/bash
# shellcheck disable=SC2038
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
# Print run information
mme2e_log "Printing Cypress container informations"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node -p 'module.paths'
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<"EOF"
cat <<INNEREOF
node version: $(node -v)
npm version: $(npm -v)
debian version: $(cat /etc/debian_version)
user: $(whoami)
chrome: $(google-chrome --version || true)
firefox: $(firefox --version || true)
INNEREOF
EOF
mme2e_log "Prepare Cypress: ensure container user entry exists for UID $MME2E_UID"
${MME2E_DC_SERVER} exec -T -- cypress bash <<EOF
getent passwd $MME2E_UID >/dev/null 2>&1 || \
useradd --uid $MME2E_UID --gid 0 --home /root --no-create-home --shell /bin/bash mme2euser 2>/dev/null || true
EOF
mme2e_log "Prepare Cypress: install dependencies"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
npm install --cache /tmp/empty-cache
EOF
# cypress install verifies the Electron binary, which needs a display and dbus session.
# Use xvfb-run and dbus-run-session for the headless virtual display and bus.
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<'EOF'
set -euo pipefail
Xvfb :99 -screen 0 1280x1024x24 -nolisten tcp &
XVFB_PID=$!
trap 'kill "$XVFB_PID" 2>/dev/null || true' EXIT
sleep 1
DISPLAY=:99 dbus-run-session -- cypress install
EOF
# Initialize cypress report directory
mme2e_log "Prepare Cypress: clean and initialize report and logs directory"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
rm -rf logs results
mkdir -p logs
mkdir -p results/junit
touch results/junit/empty.xml
echo '<?xml version="1.0" encoding="UTF-8"?>' > results/junit/empty.xml
EOF
# Run cypress test
# No need to collect its exit status: if it's nonzero, this script will terminate since we use '-e'
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}" # Remove slashes from CI_BASE_URL to produce a usable filename
# shellcheck disable=SC2016
if ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash -c '[ -n "${AUTOMATION_DASHBOARD_URL}" ]'; then
mme2e_log "AUTOMATION_DASHBOARD_URL is set. Using run_test_cycle.js for the cypress run"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node run_test_cycle.js | tee "../cypress/logs/${LOGFILE_SUFFIX}_cypress.log"
else
mme2e_log "AUTOMATION_DASHBOARD_URL is unset. Using run_tests.js for the cypress run"
# shellcheck disable=SC2086
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node run_tests.js $TEST_FILTER | tee "../cypress/logs/${LOGFILE_SUFFIX}_cypress.log"
fi
# Collect run results
cat >../cypress/results/summary.json <<EOF
{
"passed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.passes' | jq -s add),
"failed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.failures' | jq -s add),
"failed_expected": 0
}
EOF
# Collect server logs
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../cypress/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1

View File

@@ -0,0 +1,154 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
mme2e_log "Prepare Playwright: clean and initialize report and logs directory"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
cd e2e-tests/playwright
rm -rf logs results storage_state
mkdir -p logs results
touch logs/mattermost.log
EOF
mme2e_log "Prepare Playwright: install dependencies"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
cd webapp/
npm install --cache /tmp/empty-cache
cd ../e2e-tests/playwright
npm install --cache /tmp/empty-cache
EOF
mme2e_log "Prepare Playwright: environment info"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<"EOF"
cat <<INNEREOF
debian = $(cat /etc/debian_version)
uname = $(uname -m)
node = $(node -v)
npm = $(npm -v)
playwright = $(cd e2e-tests/playwright && npx playwright --version)
browsers
$(du -hs ~/ms-playwright/* | awk '{print " = " $2 " (" $1 ")"}')
INNEREOF
EOF
# Enable next line to debug Playwright
# export DEBUG=pw:protocol,pw:browser,pw:api
mme2e_log "Start LibreTranslate mock server for autotranslation tests"
${MME2E_DC_SERVER} exec -u "$MME2E_UID" -d -- playwright bash -c "cd e2e-tests/playwright && npm run start:libretranslate-mock" || true
mme2e_log "Wait for LibreTranslate mock server to be ready"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -c "for i in {1..30}; do curl -s http://localhost:3010/ && exit 0; sleep 1; done; echo 'Mock server failed to start'; exit 1" || true
# Pass TEST_FILTER and PW_SHARD with `docker compose exec -e VAR`
# (no =value) so Compose copies them from this shell's environment.
mme2e_log "Playwright: running chrome project (sharded)"
${MME2E_DC_SERVER} exec -i -T -u "$MME2E_UID" \
-e TEST_FILTER \
-e PW_SHARD \
-- playwright bash -lc "cd e2e-tests/playwright && npm run test:ci -- \${TEST_FILTER:+\$TEST_FILTER} \${PW_SHARD:+\$PW_SHARD}" | tee ../playwright/logs/playwright-first.log || true
# IMPORTANT: Playwright's blob reporter WIPES its outputDir at the start
# of every invocation. If we leave first-pass blobs inside
# `results/blob-report/`, a follow-on `npm run test:ci` deletes them.
STASH_DIR="e2e-tests/playwright/.blob-stash"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -lc "
rm -rf ${STASH_DIR}
mkdir -p ${STASH_DIR}
if compgen -G 'e2e-tests/playwright/results/blob-report/*.zip' >/dev/null 2>&1; then
for f in e2e-tests/playwright/results/blob-report/*.zip; do
mv \"\$f\" \"${STASH_DIR}/first-\$(basename \"\$f\")\"
done
fi
" || true
RESULTS_FILE="../playwright/results/reporter/results.json"
FAILED_SPECS=""
if [ -f "$RESULTS_FILE" ]; then
FAILED_SPECS=$(jq -r '
[.suites[] | . as $top |
(recurse(.suites[]?) | .specs[]? | .tests[]? |
select((.results | length) > 0) |
select((.results | last).status == "failed" or (.results | last).status == "timedOut") |
(.location.file // $top.file))
] | map(select(. != null)) | unique | join(",")
' "$RESULTS_FILE" 2>/dev/null || echo "")
fi
if [ -n "$FAILED_SPECS" ]; then
mme2e_log "Retrying failed specs on the same runner: $FAILED_SPECS"
# Split the comma-separated list into an array and shell-quote every entry
# with printf '%q' so that filenames containing shell metacharacters
# (;, &&, $(), etc.) are treated as literals by the inner bash -lc shell
# and cannot be used for command injection.
IFS=',' read -ra _spec_array <<< "$FAILED_SPECS"
SPEC_ARGS=$(printf '%q ' "${_spec_array[@]}")
${MME2E_DC_SERVER} exec -i -T -u "$MME2E_UID" \
-e TEST_FILTER \
-e PW_SHARD \
-- playwright bash -lc "cd e2e-tests/playwright && npm run test:ci -- $SPEC_ARGS \${TEST_FILTER:+\$TEST_FILTER} \${PW_SHARD:+\$PW_SHARD}" | tee ../playwright/logs/playwright-retry.log || true
# Stash retry blobs alongside the first-pass blobs.
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -lc "
if compgen -G 'e2e-tests/playwright/results/blob-report/*.zip' >/dev/null 2>&1; then
for f in e2e-tests/playwright/results/blob-report/*.zip; do
mv \"\$f\" \"${STASH_DIR}/retry-\$(basename \"\$f\")\"
done
fi
" || true
fi
# Move all stashed blobs back into blob-report/ for final merge-reports
# and for upload-artifact. This step runs whether or not retries ran:
# if no retries, we just put the first-pass blobs back.
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -lc "
mkdir -p e2e-tests/playwright/results/blob-report
if compgen -G '${STASH_DIR}/*.zip' >/dev/null 2>&1; then
mv ${STASH_DIR}/*.zip e2e-tests/playwright/results/blob-report/
fi
rmdir ${STASH_DIR} 2>/dev/null || true
" || true
# Merge the combined blob set into a single results.json so the per-shard
# `summary.json` below reflects first-pass + retry outcomes. The cross-
# shard merge in the CI template's `calculate-results` job will re-merge
# all shards' blobs from the same `blob-report/` contents.
if [ -n "$FAILED_SPECS" ]; then
mme2e_log "Merging first-pass + retry blob reports"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -lc "
cd e2e-tests/playwright
rm -rf results/reporter
mkdir -p results/reporter
npx playwright merge-reports --config merge.config.mjs results/blob-report
" || true
else
mme2e_log "No failed specs in shard; skipping inline retry"
fi
# Keep a combined tail log for backwards-compat with anything grepping
# playwright.log. The authoritative results are the merged blob reports.
cat ../playwright/logs/playwright-first.log \
../playwright/logs/playwright-retry.log \
>../playwright/logs/playwright.log 2>/dev/null || true
# Collect run results from the merged results.json. This summary is used
# only by local dev / the cypress-oriented template; the playwright CI
# template computes authoritative totals from the merged blob reports it
# downloads across all shards.
# Documentation: https://playwright.dev/docs/api/class-testcase#test-case-expected-status
if [ -f ../playwright/results/reporter/results.json ]; then
jq -f /dev/stdin ../playwright/results/reporter/results.json >../playwright/results/summary.json <<EOF
{
passed: .stats.expected,
failed: .stats.unexpected,
failed_expected: (.stats.skipped + .stats.flaky)
}
EOF
fi
# Collect server logs
${MME2E_DC_SERVER} logs --no-log-prefix -- server >../playwright/logs/mattermost.log 2>&1

113
e2e-tests/.ci/server.run_specs.sh Executable file
View File

@@ -0,0 +1,113 @@
#!/bin/bash
# shellcheck disable=SC2038
# Run specific spec files
# Usage: SPEC_FILES="path/to/spec1.ts,path/to/spec2.ts" make start-server run-specs
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
if [ -z "${SPEC_FILES:-}" ]; then
mme2e_log "Error: SPEC_FILES environment variable is required"
mme2e_log "Usage: SPEC_FILES=\"path/to/spec.ts\" make start-server run-specs"
exit 1
fi
mme2e_log "Running spec files: $SPEC_FILES"
case $TEST in
cypress)
mme2e_log "Running Cypress with specified specs"
# Initialize cypress report directory
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
rm -rf logs results
mkdir -p logs
mkdir -p results/junit
mkdir -p results/mochawesome-report/json/tests
touch results/junit/empty.xml
echo '<?xml version="1.0" encoding="UTF-8"?>' > results/junit/empty.xml
EOF
# Install dependencies
mme2e_log "Prepare Cypress: install dependencies"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
cd webapp/
npm install --cache /tmp/empty-cache
cd ../e2e-tests/cypress
npm install --cache /tmp/empty-cache
EOF
# Run cypress with specific spec files and mochawesome reporter
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}_specs"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress npx cypress run \
--spec "$SPEC_FILES" \
--reporter cypress-multi-reporters \
--reporter-options configFile=reporter-config.json \
| tee "../cypress/logs/${LOGFILE_SUFFIX}_cypress.log" || true
# Collect run results
if [ -d ../cypress/results/mochawesome-report/json/tests/ ]; then
cat >../cypress/results/summary.json <<EOF
{
"passed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.passes' | jq -s add),
"failed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.failures' | jq -s add),
"failed_expected": 0
}
EOF
fi
# Collect server logs
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../cypress/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1
;;
playwright)
mme2e_log "Running Playwright with specified specs"
# Convert comma-separated to space-separated for playwright
SPEC_ARGS=$(echo "$SPEC_FILES" | tr ',' ' ')
# Initialize playwright report and logs directory
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
cd e2e-tests/playwright
rm -rf logs results storage_state
mkdir -p logs results
touch logs/mattermost.log
EOF
# Install dependencies
mme2e_log "Prepare Playwright: install dependencies"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
cd webapp/
npm install --cache /tmp/empty-cache
cd ../e2e-tests/playwright
npm install --cache /tmp/empty-cache
EOF
# Run playwright with specific spec files
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}_specs"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" \
-e TEST_FILTER \
-e PW_SHARD \
-- playwright bash -lc "cd e2e-tests/playwright && npm run test:ci -- $SPEC_ARGS \${TEST_FILTER:+\$TEST_FILTER} \${PW_SHARD:+\$PW_SHARD}" | tee "../playwright/logs/${LOGFILE_SUFFIX}_playwright.log" || true
# Collect run results (if results.json exists)
if [ -f ../playwright/results/reporter/results.json ]; then
jq -f /dev/stdin ../playwright/results/reporter/results.json >../playwright/results/summary.json <<EOF
{
passed: .stats.expected,
failed: .stats.unexpected,
failed_expected: (.stats.skipped + .stats.flaky)
}
EOF
mme2e_log "Results file found and summary generated"
fi
# Collect server logs
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../playwright/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1
;;
*)
mme2e_log "Error, unsupported value for TEST: $TEST" >&2
mme2e_log "Aborting" >&2
exit 1
;;
esac
mme2e_log "Spec run complete"

View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
case $TEST in
cypress)
./server.run_cypress.sh
;;
playwright)
./server.run_playwright.sh
;;
none)
mme2e_log "Running with TEST=$TEST. No tests to run."
exit 0
;;
*)
mme2e_log "Error, unsupported value for TEST: $TEST" >&2
mme2e_log "Aborting" >&2
exit 1
;;
esac

123
e2e-tests/.ci/server.start.sh Executable file
View File

@@ -0,0 +1,123 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
# Wait for the required server image
mme2e_log "Waiting for server image to be available"
mme2e_wait_image "$SERVER_IMAGE" 4 30
# Launch mattermost-server, and wait for it to be healthy
mme2e_log "Starting E2E containers"
${MME2E_DC_SERVER} create
# `docker compose up -d` returns non-zero the moment any depended container
# exits during startup, which masks openldap's own `restart: always` policy.
# On a small fraction of ubuntu-24.04 runners the osixia/openldap:1.4.0 image
# exits 1 on first boot (suspected init-script race under runner load). Retry
# the `up` a bounded number of times, force-recreating openldap between tries
# so its first-boot bootstrap re-runs cleanly, and dump rich diagnostics on
# every failure so future CI failures contain the actual data we need to
# permanently root-cause this. The diagnostics directory is also uploaded as
# a workflow artifact (see e2e-tests-*-template.yml `ci/upload-docker-diagnostics`).
DIAG_DIR="${PWD}/../docker-diagnostics"
mkdir -p "$DIAG_DIR"
dump_openldap_diagnostics() {
local label="$1"
local out="$DIAG_DIR/${label}"
mkdir -p "$out"
mme2e_log "[diagnostics:${label}] capturing openldap state to $out"
# Container-level state (exit code, OOMKilled, error string, restart count)
docker inspect mmserver-openldap-1 >"$out/openldap.inspect.json" 2>&1 || true
${MME2E_DC_SERVER} ps -a >"$out/compose.ps.txt" 2>&1 || true
${MME2E_DC_SERVER} logs --no-log-prefix -- openldap >"$out/openldap.log" 2>&1 || true
# Merged compose config — confirms which security_opt / cap_add / image is actually applied
${MME2E_DC_SERVER} config >"$out/compose.config.yml" 2>&1 || true
# Host-level state useful for OOM / AppArmor diagnosis
uname -a >"$out/host.uname.txt" 2>&1 || true
free -m >"$out/host.free.txt" 2>&1 || true
df -h >"$out/host.df.txt" 2>&1 || true
docker version >"$out/docker.version.txt" 2>&1 || true
docker info >"$out/docker.info.txt" 2>&1 || true
docker compose version >"$out/compose.version.txt" 2>&1 || true
cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns >"$out/host.apparmor_userns.txt" 2>&1 || true
# AppArmor denials and OOM kills land in dmesg — grep them out (needs sudo on GH runners).
sudo dmesg | tail -200 >"$out/host.dmesg.tail.txt" 2>&1 || true
sudo dmesg | grep -iE 'apparmor|denied|oom|killed|openldap|slapd' >"$out/host.dmesg.relevant.txt" 2>&1 || true
# Echo the most useful slice straight to the workflow log so it shows up
# in the GH Actions UI without needing to download the artifact.
mme2e_log "----- openldap inspect (exit/oom/error) -----"
docker inspect mmserver-openldap-1 \
--format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} Restarts={{.RestartCount}} Status={{.State.Status}}' \
2>&1 || true
mme2e_log "----- openldap log (last 100) -----"
${MME2E_DC_SERVER} logs --no-log-prefix --tail=100 -- openldap 2>&1 || true
mme2e_log "----- relevant dmesg -----"
sudo dmesg | grep -iE 'apparmor|denied|oom|killed|openldap|slapd' | tail -40 2>&1 || true
mme2e_log "----- end diagnostics:${label} -----"
}
UP_ATTEMPTS=3
for attempt in $(seq 1 $UP_ATTEMPTS); do
if ${MME2E_DC_SERVER} up -d --remove-orphans; then
break
fi
dump_openldap_diagnostics "up-attempt-${attempt}"
if [ "$attempt" -eq "$UP_ATTEMPTS" ]; then
mme2e_log "compose up failed after ${UP_ATTEMPTS} attempts; aborting"
exit 1
fi
# Force-recreate openldap so its first-boot init re-runs from a clean state
${MME2E_DC_SERVER} rm -fsv openldap || true
sleep 5
done
# Postgres check
if ! mme2e_wait_command_success "${MME2E_DC_SERVER} exec -T -- postgres pg_isready -h localhost" "Waiting for postgres to accept connections" "30" "5"; then
mme2e_log "Postgres not accepting connections"
exit 1
fi
if ! mme2e_wait_service_healthy server 60 10; then
mme2e_log "Mattermost container not healthy, retry attempts exhausted. Giving up." >&2
exit 1
fi
# shellcheck disable=SC2043
for MIGRATION in migration_advanced_permissions_phase_2; do
# Query explanation: if it doesn't find the migration in the table, there are 0 results and the command fails with a divide-by-zero error. Otherwise the command succeeds
MIGRATION_CHECK_COMMAND="${MME2E_DC_SERVER} exec -T postgres sh -c 'PGPASSWORD=mostest_password psql -U mmuser mattermost_test -c \"select 1 / (select count(*) from Systems where name = '\''${MIGRATION}'\'' and value = '\''true'\'');\"'"
if ! mme2e_wait_command_success "$MIGRATION_CHECK_COMMAND" "Waiting for migration to be completed: ${MIGRATION}" "10" "10"; then
mme2e_log "Migration ${MIGRATION} not completed, retry attempts exhausted. Giving up." >&2
LOG_DIR="../${TEST}/logs"
mkdir -p "$LOG_DIR"
# Save server logs to a file
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"$LOG_DIR/server.log"
mme2e_log "Server logs saved to server.log"
# Save postgres logs to a file
${MME2E_DC_SERVER} logs --no-log-prefix -- postgres >"$LOG_DIR/postgres.log"
mme2e_log "Postgres logs saved to postgres.log"
exit 2
fi
mme2e_log "${MIGRATION}: completed."
done
mme2e_log "Mattermost container is running and healthy"
# Wait for webhook-interactions container if running cypress or playwright tests
if [ "$TEST" = "cypress" ] || [ "$TEST" = "playwright" ]; then
mme2e_log "Checking webhook-interactions container health"
${MME2E_DC_SERVER} logs --no-log-prefix -- webhook-interactions 2>&1 | tail -5
if ! mme2e_wait_service_healthy webhook-interactions 2 10; then
mme2e_log "Webhook interactions container not healthy, retry attempts exhausted. Giving up." >&2
exit 1
fi
mme2e_log "Webhook interactions container is running and healthy"
fi

7
e2e-tests/.ci/server.stop.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -e -u -o pipefail
cd "$(dirname "$0")"
. .e2erc
mme2e_log "Stopping E2E containers"
${MME2E_DC_SERVER} down -v --remove-orphans

14
e2e-tests/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# env, cert, key, license
.env*
*.crt
*.key
*.license
# Plugin
*.tar.gz
# node
*.lock
# build
*.tsbuildinfo

56
e2e-tests/Makefile Normal file
View File

@@ -0,0 +1,56 @@
SHELL := /bin/bash
.PHONY: all run stop clean
all: run
run: generate-server start-server run-test
stop: stop-server stop-dashboard clean
report: collect-report-data publish-report
clean:
rm -fv .ci/server.yml
rm -fv .ci/.env.{server,dashboard,cypress,playwright}
.PHONY: generate-server start-server run-test run-specs stop-server restart-server
generate-server:
bash ./.ci/server.generate.sh
start-server: generate-server
bash ./.ci/server.start.sh
bash ./.ci/server.prepare.sh
run-test:
bash ./.ci/server.run_test.sh
run-specs:
bash ./.ci/server.run_specs.sh
stop-server: generate-server
bash ./.ci/server.stop.sh
restart-server: stop-server start-server
.PHONY: start-dashboard generate-test-cycle stop-dashboard
start-dashboard:
bash ./.ci/dashboard.start.sh
generate-test-cycle: requirecmd-node generate-server
bash ./.ci/dashboard.generate_test_cycle.sh
stop-dashboard:
bash ./.ci/dashboard.stop.sh
.PHONY: cloud-init cloud-teardown
cloud-init: requirecmd-jq requirecmd-curl
bash ./.ci/server.cloud_init.sh
cloud-teardown:
bash ./.ci/server.cloud_teardown.sh
.PHONY: collect-report-data publish-report
collect-report-data:
bash ./.ci/report.collect.sh
publish-report: requirecmd-node
bash ./.ci/report.publish.sh
.PHONY: check-shell fmt-shell fmt-node fmt
requirecmd-%:
@which "$(*)" >/dev/null || { echo "Error, missing required CLI tool: $(*). Aborting." >&2; exit 1; }
check-shell: requirecmd-shellcheck
shellcheck ./.ci/*.sh ./.ci/.e2erc* # Install with https://webinstall.dev/shfmt/
fmt-shell: requirecmd-shfmt
shfmt -w -s -i 2 ./.ci/*.sh # Install with https://webinstall.dev/shellcheck/
fmt-node: requirecmd-npx
# Formats yaml files
npx prettier ./.ci "!./.ci/dashboard" --write --cache
fmt: fmt-node fmt-shell

76
e2e-tests/README.md Normal file
View File

@@ -0,0 +1,76 @@
# E2E testing for the Mattermost web client
This directory contains the E2E testing code for the Mattermost web client.
### How to run locally
##### For test case development
Please refer to the [dedicated developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/e2e-testing/) for instructions.
> **Playwright note:** the instructions below describe the Docker Compose flow Cypress uses (and that Playwright previously used too). Playwright's CI and recommended local setup have since moved to [Testcontainers](https://node.testcontainers.org/) — see `playwright/README.md`'s Server Setup section for details. This Compose flow remains the way to run Cypress, and to run a plain server instance (`TEST=none make`).
##### For pipeline debugging
The E2E testing pipeline's scripts depend on the following tools being installed on your system: `docker`, `docker-compose`, `make`, `git`, `jq`, `node`, and some common utilities (`coreutils`, `findutils`, `bash`, `awk`, `sed`, `grep`)
Instructions, tl;dr: create a local branch with your E2E test changes, then open a PR to the `mattermost-server` repo targeting the `master` branch (so that CI will produce the image that docker-compose needs), then run `make` in this directory.
Instructions, detailed:
1. (optional, undefined variables are set to sane defaults) Create the `.ci/env` file, and populate it with the variables you need out of the following list:
* `SERVER`: either `onprem` (default) or `cloud`.
* `CWS_URL` (mandatory when `SERVER=cloud`, only used in such case): when spinning up a cloud-like test server that communicates with a test instance of a customer web server.
* `TEST`: either `cypress` (default), `playwright`, or `none` (to avoid creating the cypress/playwright sidecar containers, e.g. if you only want to launch a server instance)
* `ENABLED_DOCKER_SERVICES`: a space-separated list of services to start alongside the server. Default to `postgres inbucket`, for smoke test purposes and for lightweight and faster start-up time. Depending on the test requirement being worked on, you may want to override as needed, as such:
- Cypress full tests require all services to be running: `postgres inbucket minio openldap elasticsearch keycloak`.
- Cypress smoke tests require only the following: `postgres inbucket`.
- Playwright no longer runs against this Compose flow in CI (see the note above) — this only matters if you're using it to spin up a plain server instance (`TEST=none make`) for Playwright's `external` mode.
* The following variables, will be passed over to the server container: `MM_LICENSE` (no enterprise features will be available if this is unset; required when `SERVER=cloud`), and the exploded `MM_ENV` (a comma-separated list of env var specifications)
* The following variables, which will be passed over to the cypress container: `BRANCH`, `BUILD_ID`, `CI_BASE_URL`, `BROWSER`, `AUTOMATION_DASHBOARD_URL` and `AUTOMATION_DASHBOARD_TOKEN`
* The `SERVER_IMAGE` variable can also be set if you want to select a custom mattermost-server image. If not specified, the value of the `SERVER_IMAGE_DEFAULT` variable defined in file `.ci/.e2erc` is used.
* The `TEST_FILTER` variable can also be set, to customize which tests you want Cypress/Playwright to run. If not specified, only the smoke tests will run
- Its format depends on which tool is used: for Cypress, please check the `e2e-tests/cypress/run_tests.js` file for details. For Playwright, it can simply be populated with arguments you want to give to the `playwright test` command.
* More variables may be required to configure reporting and cloud interactions. Check the content of the `.ci/report.*.sh` and `.ci/server.cloud_*.sh` scripts for reference.
2. (optional) `make start-dashboard && make generate-test-cycle`: start the automation dashboard in the background, and initiate a test cycle on it, for the given `BUILD_ID`
* NB: the `BUILD_ID` value should stay the same across the `make generate-test-cycle` command, and the subsequent `make` (see next step). If you need to initiate a new test cycle on the same dashboard, you'll need to change the `BUILD_ID` value and rerun both `make generate-test-cycle` and `make`.
* Note that part of the dashboard functionality assumes the `BUILD_ID` to have a certain format (see [here](https://github.com/saturninoabril/automation-dashboard/blob/175891781bf1072c162c58c6ec0abfc5bcb3520e/lib/common_utils.ts#L3-L23) for details). This is not relevant for local running, but it's important to note in the testing pipelines.
* This also automatically sets the `AUTOMATION_DASHBOARD_URL` and `AUTOMATION_DASHBOARD_TOKEN` variables for the cypress container
* Note that if you run the dashboard locally, but also specify other `AUTOMATION_DASHBOARD_*` variables in your `.ci/env` file, the latter variables will take precedence.
* The dashboard is used for orchestrating specs with parallel test runs and is typically used in CI.
* Only Cypress is currently using the dashboard; Playwright is not.
3. `make`: start and prepare the server, then run the Cypress smoke tests
* You can track the progress of the run in the `http://localhost:4000/cycles` dashboard if you launched it locally
* For `SERVER=cloud` runs, you'll need to first create a cloud customer against the specified `CWS_URL` service by running `make cloud-init`. The user isn't automatically removed, and may be reused across multiple runs until you run `make cloud-teardown` to delete it.
* If you want to run the Playwright tests instead of the Cypress ones, you can run `TEST=playwright make` — though `playwright/README.md`'s Testcontainers option is now the recommended way to run Playwright locally/in CI
* If you just want to run a local server instance, without any further testing, you can run `TEST=none make`
* If you're using the automation dashboard, you have the option of sharding the E2E test run: you can launch the `make` command in parallel on different machines (NB: you must use the same `BUILD_ID` and `BRANCH` values that you used for `make generate-test-cycle`) to distribute running the test cases across them. When doing this, you should also set on each machine the `CI_BASE_URL` variable to a value that uniquely identifies the instance where `make` is running.
* This script will also parse the local test results, and write a `e2e-tests/${TEST}/results/summary.json` file containing the following keys: `passed`, `failed` and `failed_expected` (the total number of testcases that were run is the sum of these three numbers)
4. `make stop`: tears down the server (and the dashboard, if running)
* This will stop and cleanup all of the E2E testing containers, including the database and its persistent volume.
* This also implicitly runs `make clean`, which also removes any generated environment or docker-compose files.
Notes:
- Setting a variable in `.ci/env` is functionally equivalent to exporting variables in your current shell's environment, before invoking the makefile.
- The `.ci/.env.*` files are auto-generated by the pipeline scripts and aren't meant to be modified manually. The only file you should edit to control the containers' environment is `.ci/env`, as specified in the instructions above.
- All of the variables in `.ci/env` must be set before the `make generate-server` command is run (or, if using the dashboard, before the `make generate-test-cycle` command). Modifying that file afterward has no effect because the containers' env files are generated in that step.
- If you restart the dashboard at any point, you must also restart the server containers, so that it picks up the new IP of the dashboard from the newly generated `.env.dashboard` file
- If new variables need to be passed to any of the containers, here are the general principles to follow when deciding where to populate it:
* If their value is fixed (e.g. a static server configuration), these may be simply added to the `docker_compose_generator.sh` file, to the appropriate container.
* If you need to introduce variables that you want to control from `.ci/env`: you need to update the scripts under the `.ci/` dir and configure them to write the new variables' values over to the appropriate `.env.*` file. In particular, avoid defining variables that depend on other variables within the docker-compose override files: this is to ensure uniformity in their availability and simplifies the question of what container has access to which variable considerably.
* Exceptions are of course accepted wherever it makes sense (e.g. if you need to group variables based on some common functionality)
- The `report` Make target is meant for internal usage. Usage and variables are documented in the respective scripts.
- `make start-server` won't cleanup containers that don't change across runs. This means that you can use it to emulate a Mattermost server upgrade while retaining your database data by simply changing the `SERVER_IMAGE` variable on your machine, and then re-running `make start-server`. But this also means that if you want to run a clean local environment, you may have to manually run `make stop` to cleanup any running containers and their volumes, which include e.g. the database.
##### For code changes:
* `make fmt-ci` to format and check yaml files and shell scripts.
##### For test stressing an E2E testcase
For Cypress:
1. Enter the `cypress/` subdirectory
2. Identify which test files you want to run, and how many times each. For instance: suppose you want to run `create_a_team_spec.js` and `demoted_user_spec.js` (which you can locate with the `find` command, under `cypress/tests/`), each run 3 times
3. Run the chosen testcases the desired amount of times: `node run_tests.js --include-file=create_a_team_spec.js,demoted_user_spec.js --invert --stress-test-count=3`
* Your system needs to be setup for Cypress usage, to be able to run this command. Refer to the [E2E testing developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/e2e-testing/) for this.
4. The `cypress/results/testPasses.json` file will count, for each of the testfiles, how many times it was run, and how many times each of the testcases contained in it passed. If the attempts and passes numbers do not match, that specific testcase may be flaky.
For Playwright: not currently supported — Playwright tests are run and re-run individually via `npm run test -- <spec>`, see `playwright/README.md`.

View File

@@ -0,0 +1,13 @@
FROM node:14-alpine
RUN apk update && apk upgrade && \
apk add --no-cache bash git openssh
WORKDIR /usr/src
RUN npm install axios express client-oauth2@larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49
COPY ./tests/plugins/post_message_as.js /usr/src/tests/plugins/post_message_as.js
COPY ./utils/webhook_utils.js /usr/src/utils/webhook_utils.js
COPY ./webhook_serve.js /usr/src
EXPOSE 3000
CMD [ "node", "webhook_serve.js" ]

View File

@@ -0,0 +1,132 @@
# Testing with subpath servers
Some tests need multiple servers running in subpath mode. These tests have the cypress `Group: @subpath` metadata near the top of the test file. Instructions on running a server under subpath can be found here: [https://developers.mattermost.com/blog/subpath/](https://developers.mattermost.com/blog/subpath/)
In the `cypress.json` configuration file, the `baseURL` setting will need to be updated with the subpath URL of the first server, and the `secondServerURL` setting with the subpath URL of the second server.
### Running subpath tests on local machine
Two mattermost servers running on the same machine must be served from different ports. To have the servers respond on the same URL and the same port under different subpaths, you will need to use a reverse proxy (nginx or apache) to proxy the same local url to both mattermost servers under different subpaths.
#### Example set up using NGINX:
You'll need to run two Mattermost servers.
1. Set the `SiteURL` and the listening port for the first server:
```
"SiteURL": "http://localhost/company/mattermost1"
"ListenAddress": ":8065",
```
2. Set the `SiteURL` and the listening port for the second server:
```
"SiteURL": "http://localhost/company/mattermost2"
"ListenAddress": ":8066",
```
The DB `DataSource` will need to be different for both servers.
3. Install NGINX - exact steps depend on your OS
4. Update your NGINX site configuration. The specific details for each setting can be found in the [Mattermost docs](https://docs.mattermost.com/install/config-proxy-nginx.html)
```
upstream backend1 {
server localhost:8065;
keepalive 32;
}
upstream backend2 {
server localhost:8066;
keepalive 32;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
location ~ /company/mattermost1/api/v[0-9]+/(users/)?websocket$ {
client_body_timeout 60;
client_max_body_size 50M;
lingering_timeout 5;
proxy_buffer_size 16k;
proxy_buffers 256 16k;
proxy_connect_timeout 90;
proxy_pass http://backend1;
proxy_read_timeout 90s;
proxy_send_timeout 300;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_set_header X-Real-IP $remote_addr;
send_timeout 300;
}
location /company/mattermost1 {
client_max_body_size 50M;
proxy_buffer_size 16k;
proxy_buffers 256 16k;
proxy_cache_lock on;
proxy_cache_min_uses 2;
proxy_cache_revalidate on;
proxy_cache_use_stale timeout;
proxy_http_version 1.1;
proxy_pass http://backend1;
proxy_read_timeout 600s;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_set_header X-Real-IP $remote_addr;
}
location ~ /company/mattermost2/api/v[0-9]+/(users/)?websocket$ {
client_body_timeout 60;
client_max_body_size 50M;
lingering_timeout 5;
proxy_buffer_size 16k;
proxy_buffers 256 16k;
proxy_connect_timeout 90;
proxy_pass http://backend2;
proxy_read_timeout 90s;
proxy_send_timeout 300;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_set_header X-Real-IP $remote_addr;
send_timeout 300;
}
location /company/mattermost2 {
proxy_buffer_size 16k;
proxy_buffers 256 16k;
proxy_cache_lock on;
proxy_cache_min_uses 2;
proxy_cache_revalidate on;
proxy_cache_use_stale timeout;
proxy_http_version 1.1;
proxy_pass http://backend2;
proxy_read_timeout 600s;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 50M;
}
}
```
5. Restart NGINX to reload the configuration. Exact steps depend on your OS/distribution. On most Linux distributions you can run `sudo systemctl restart nginx`
6. In the `cypress.json` file, set `baseURL` to `"http://localhost/company/mattermost1"` and `secondServerURL` to `"http://localhost/company/mattermost2"`
7. Start both Mattermost tests and run the e2e tests.

View File

@@ -0,0 +1,127 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as path from 'path';
import {defineConfig} from 'cypress';
export default defineConfig({
chromeWebSecurity: false,
defaultCommandTimeout: 30000,
downloadsFolder: 'tests/downloads',
fixturesFolder: 'tests/fixtures',
numTestsKeptInMemory: 0,
retries: {
// retries for cypress:run
runMode: 1,
// No retries for cypress:open
openMode: 0,
},
screenshotsFolder: 'tests/screenshots',
taskTimeout: 60000,
video: true,
videoCompression: true,
videosFolder: 'tests/videos',
viewportWidth: 1300,
allowCypressEnv: false,
expose: {
adminEmail: process.env.CYPRESS_adminEmail || 'sysadmin@sample.mattermost.com',
adminUsername: process.env.CYPRESS_adminUsername || 'sysadmin',
adminPassword: process.env.CYPRESS_adminPassword || 'Sys@dmin-sample1',
allowedUntrustedInternalConnections: 'localhost',
cwsURL: 'http://localhost:8076',
cwsAPIURL: 'http://localhost:8076',
dbClient: 'postgres',
dbConnection: process.env.CYPRESS_dbConnection || 'postgres://mmuser:mostest_password@localhost/mattermost_test?sslmode=disable&connect_timeout=10',
elasticsearchConnectionURL: 'http://localhost:9200',
firstTest: false,
keycloakAppName: 'mattermost',
keycloakBaseUrl: 'http://localhost:8484',
keycloakUsername: 'mmuser',
keycloakPassword: 'mostest_password',
ldapServer: 'localhost',
ldapPort: 389,
minioAccessKey: 'minioaccesskey',
minioSecretKey: 'miniosecretkey',
minioS3Bucket: 'mattermost-test',
minioS3Endpoint: 'localhost:9000',
minioS3SSL: false,
numberOfTrialUsers: 100,
pushNotificationServer: 'https://push-test.mattermost.com',
resetBeforeTest: false,
runLDAPSync: true,
secondServerURL: 'http://localhost/s/p',
serverEdition: 'Team',
serverClusterEnabled: false,
serverClusterName: 'mm_dev_cluster',
serverClusterHostCount: 3,
smtpUrl: process.env.CYPRESS_smtpUrl || 'http://localhost:9001',
webhookBaseUrl: 'http://localhost:3000',
},
e2e: {
setupNodeEvents(on, config) {
// Custom webpack preprocessor for TypeScript 6 + @/ path aliases.
// @cypress/webpack-preprocessor@7.1.0 already handles TS6's
// downlevelIteration deprecation (cypress-io/cypress#33575).
// We add: ts-loader for .ts files, resolve extensions, and @/ alias.
// When Cypress >= 15.14 ships, the only custom piece needed is the @/ alias.
const webpackPreprocessor = require('@cypress/webpack-preprocessor');
const webpackOptions = {
resolve: {
extensions: ['.ts', '.js', '.json'],
alias: {
'@': path.resolve(__dirname, 'tests'),
},
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
],
},
],
},
// Restore Node.js polyfills (crypto, process, path, etc.)
// that webpack 5 no longer provides by default.
plugins: [
new (require('node-polyfill-webpack-plugin'))(),
new (require('webpack').ProvidePlugin)({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
],
};
on('file:preprocessor', webpackPreprocessor({webpackOptions}));
return require('./tests/plugins/index.js')(on, config);
},
baseUrl: process.env.MM_SERVICESETTINGS_SITEURL || 'http://localhost:8065',
excludeSpecPattern: '**/node_modules/**/*',
specPattern: 'tests/integration/**/*_spec.{js,ts}',
supportFile: 'tests/support/index.js',
testIsolation: false,
},
});

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import pluginCypress from 'eslint-plugin-cypress';
import noOnlyTest from 'eslint-plugin-no-only-tests';
import globals from 'globals';
import eslintPlugin from '@mattermost/eslint-plugin';
export default [
{
ignores: ['**/node_modules', '**/logs', '**/results'],
},
...eslintPlugin.configs.base,
{
files: ['**/*.ts', '**/*.js'],
plugins: {
cypress: pluginCypress,
'no-only-tests': noOnlyTest,
},
languageOptions: {
globals: {
...globals.chai,
...globals.node,
...globals.mocha,
Cypress: 'readonly',
cy: 'readonly',
},
},
settings: {
'import/resolver': {
node: true,
},
},
rules: {
'cypress/assertion-before-screenshot': 'warn',
'cypress/no-assigning-return-values': 'error',
'cypress/no-force': 'off',
'cypress/no-async-tests': 'error',
'cypress/no-pause': 'error',
'cypress/no-unnecessary-waiting': 0,
'cypress/unsafe-to-chain-command': 0,
'func-names': 0,
'global-require': 0,
'@stylistic/lines-around-comment': 0,
'max-nested-callbacks': 0,
'no-await-in-loop': 0,
'no-loop-func': 0,
'no-unused-expressions': 0,
'no-process-env': 0,
'no-duplicate-imports': 0,
'no-undefined': 0,
'no-use-before-define': 0,
'no-only-tests/no-only-tests': ['error', {focus: ['only']}],
'no-console': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-var-requires': 'off',
'headers/header-format': [
'error',
{
source: 'string',
style: 'line',
content: 'Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.\nSee LICENSE.txt for license information.',
trailingNewlines: 2,
},
],
'import/no-duplicates': 2,
'import/order': [
'error',
{
'newlines-between': 'always',
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
},
],
'import/no-unresolved': 'off',
'max-lines': ['warn', {max: 800, skipBlankLines: true, skipComments: true}],
'@stylistic/eol-last': ['error', 'always'],
'@stylistic/no-trailing-spaces': 'error',
},
},
];

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
/*
* This command, which is normally used in CI, generates test cycle in full or partial
* depending on test metadata and environment capabilities into the Test Automation Dashboard.
* Such generated test cycle is then used to run each spec file by "node run_test_cycle.js".
*
* Usage: [ENVIRONMENT] node generate_test_cycle.js [options]
*
* Options:
* --stage=[stage]
* Selects spec files with matching stage. It can be of multiple values separated by comma.
* E.g. "--stage='@prod,@dev'" will select files with either @prod or @dev.
* --group=[group]
* Selects spec files with matching group. It can be of multiple values separated by comma.
* E.g. "--group='@channel,@messaging'" will select files with either @channel or @messaging.
* --invert
* Selected files are those not matching any of the specified stage or group.
* --include-group=[group]
* Include spec files with matching group. It can be of multiple values separated by comma.
* E.g. "--include-group='@enterprise'" will select files including @enterprise.
* --exclude-group=[group]
* Exclude spec files with matching group. It can be of multiple values separated by comma.
* E.g. "--exclude-group='@enterprise'" will select files except @enterprise.
* --include-file=[filename or directory]
* Include spec files with matching directory or filename pattern. Uses `find` command under the hood. It can be of multiple values separated by comma.
* E.g. "--include-file='channel'" will include files recursively under `channel` directory/s.
* E.g. "--include-file='*channel*'" will include files and files under directory/s recursively that matches the name with `*channel*`.
* --exclude-file=[filename or directory]
* Exclude spec files with matching directory or filename pattern. Uses `find` command under the hood. It can be of multiple values separated by comma.
* E.g. "--exclude-file='channel'" will exclude files recursively under `channel` directory/s.
* E.g. "--exclude-file='*channel*'" will exclude files and files under directory/s recursively that matches the name with `*channel*`.
*
* Environment:
* AUTOMATION_DASHBOARD_URL : Dashboard URL
* AUTOMATION_DASHBOARD_TOKEN : Dashboard token
* REPO : Project repository, ex. mattermost-webapp
* BRANCH : Branch identifier from CI
* BUILD_ID : Build identifier from CI
* BROWSER : Chrome by default. Set to run test on other browser such as chrome, edge, electron and firefox.
* The environment should have the specified browser to successfully run.
* HEADLESS : Headless by default (true) or false to run on headed mode.
*
* Example:
* 1. "node generate_test_cycle.js"
* - will create test cycle based on default test environment, except those matching skipped metadata
* 2. "node generate_test_cycle.js --stage='@prod'"
* - will create test cycle for production tests, except those matching skipped metadata
* 3. "node generate_test_cycle.js --stage='@prod' --invert"
* - will create test cycle for all non-production tests
* 4. "BROWSER='chrome' HEADLESS='false' node generate_test_cycle.js --stage='@prod' --group='@channel,@messaging'"
* - will create test cycle for spec files matching stage and group values in Chrome (headed)
* 5. "node generate_test_cycle.js --stage='@prod' --exclude-group='@enterprise'"
* - will create test cycle for all production tests except @enterprise group
* - typical test run for Team Edition
* 6. "node generate_test_cycle.js --stage='@prod' --sort-first='@elasticsearch' --sort-last='@mfa'"
* - will create test cycle for all production tests with specs specifically ordered as first and last
*/
const os = require('os');
const chalk = require('chalk').default;
const {createAndStartCycle} = require('./utils/dashboard');
const {getSortedTestFiles} = require('./utils/file');
require('dotenv').config();
const {
BRANCH,
BROWSER,
BUILD_ID,
HEADLESS,
REPO,
} = process.env;
async function main() {
const browser = BROWSER || 'chrome';
const headless = typeof HEADLESS === 'undefined' ? true : HEADLESS === 'true';
const platform = os.platform();
const {weightedTestFiles} = getSortedTestFiles(platform, browser, headless);
if (!weightedTestFiles.length) {
console.log(chalk.red('Nothing to test!'));
return;
}
const data = await createAndStartCycle({
repo: REPO,
branch: BRANCH,
build: BUILD_ID,
files: weightedTestFiles,
});
console.log(chalk.green('Successfully generated a test cycle.'));
console.log(data.cycle);
}
main();

View File

@@ -0,0 +1,93 @@
{
"name": "cypress",
"devDependencies": {
"@cypress/request": "4.0.1",
"@cypress/webpack-preprocessor": "7.1.2",
"@mattermost/client": "11.9.0",
"@mattermost/eslint-plugin": "file:../../webapp/platform/eslint-plugin",
"@mattermost/types": "11.9.0",
"@testing-library/cypress": "10.1.3",
"@types/authenticator": "1.1.4",
"@types/express": "5.0.6",
"@types/fs-extra": "11.0.4",
"@types/lodash": "4.17.25",
"@types/lodash.intersection": "4.4.9",
"@types/lodash.mapkeys": "4.6.9",
"@types/lodash.without": "4.4.9",
"@types/mime-types": "3.0.1",
"@types/mochawesome": "6.2.5",
"@types/shelljs": "0.10.0",
"authenticator": "1.1.5",
"axios": "1.19.0",
"chai": "6.2.2",
"chalk": "6.0.0",
"client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"cross-env": "10.1.0",
"cypress": "15.20.1",
"cypress-file-upload": "5.0.8",
"cypress-multi-reporters": "2.0.5",
"cypress-plugin-tab": "2.0.0",
"cypress-real-events": "1.15.0",
"cypress-wait-until": "3.0.2",
"dayjs": "1.11.23",
"deepmerge": "4.3.1",
"dotenv": "17.4.2",
"eslint": "9.39.3",
"eslint-plugin-cypress": "6.4.1",
"eslint-plugin-no-only-tests": "3.4.0",
"express": "5.2.1",
"extract-zip": "2.0.1",
"globals": "17.11.0",
"jiti": "2.7.0",
"knex": "3.3.0",
"localforage": "1.10.0",
"lodash.intersection": "4.4.0",
"lodash.mapkeys": "4.6.0",
"lodash.without": "4.4.0",
"lodash.xor": "4.5.0",
"mime": "4.1.0",
"mime-types": "3.0.2",
"mocha": "11.8.0",
"mocha-junit-reporter": "2.2.1",
"mocha-multi-reporters": "1.5.1",
"mochawesome": "8.0.1",
"mochawesome-merge": "5.1.1",
"mochawesome-report-generator": "6.3.2",
"moment-timezone": "0.6.3",
"node-polyfill-webpack-plugin": "4.1.0",
"pdf-parse": "2.4.5",
"pg": "8.23.0",
"shelljs": "0.10.0",
"timezones.json": "1.7.2",
"ts-loader": "9.6.2",
"typescript": "6.0.3",
"uuid": "14.0.1",
"yargs": "18.1.0"
},
"overrides": {
"@mattermost/client": {
"typescript": "^6"
},
"@mattermost/types": {
"typescript": "^6"
}
},
"scripts": {
"check-types": "tsc -b",
"cypress:open": "cross-env TZ=Etc/UTC cypress open",
"cypress:run": "cross-env TZ=Etc/UTC cypress run",
"cypress:run:chrome": "cross-env TZ=Etc/UTC cypress run --browser chrome",
"cypress:run:firefox": "cross-env TZ=Etc/UTC cypress run --browser firefox",
"cypress:run:edge": "cross-env TZ=Etc/UTC cypress run --browser edge",
"cypress:run:electron": "cross-env TZ=Etc/UTC cypress run --browser electron",
"benchmarks:run-server": "cd mattermost && bin/mattermost",
"start:webhook": "node webhook_serve.js",
"pretest": "npm run clean",
"test": "cross-env TZ=Etc/UTC cypress run",
"test:smoke": "node run_tests.js --stage='@prod' --group='@smoke'",
"test:ci": "node run_tests.js",
"uniq-meta": "grep -r \"^// $META:\" cypress | grep -ow '@\\w*' | sort | uniq",
"check": "eslint .",
"fix": "eslint . --quiet --fix --cache"
}
}

View File

@@ -0,0 +1,15 @@
{
"reporterEnabled": "mocha-junit-reporter, mochawesome",
"mochaJunitReporterReporterOptions": {
"mochaFile": "results/junit/test_results[hash].xml",
"toConsole": false
},
"mochawesomeReporterOptions": {
"reportDir": "results/mochawesome-report",
"reportFilename": "json/tests/[name]",
"quiet": true,
"overwrite": false,
"html": false,
"json": true
}
}

View File

@@ -0,0 +1,273 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
/*
* This command, which is normally used in CI, runs Cypress test in full or partial
* depending on test metadata and environment capabilities.
* Spec file to run is dependent from Test Automation Dashboard and each test result is being
* recorded on a per spec file basis.
*
* Usage: [ENVIRONMENT] node run_test_cycle.js
*
* Environment:
* AUTOMATION_DASHBOARD_URL : Dashboard URL
* AUTOMATION_DASHBOARD_TOKEN : Dashboard token
* REPO : Project repository, ex. mattermost-webapp
* BRANCH : Branch identifier from CI
* BUILD_ID : Build identifier from CI
* CI_BASE_URL : Test server base URL in CI
*
* Example:
* 1. "node run_test_cycle.js"
* - will run all the specs available from the Automation dashboard
*/
const chalk = require('chalk').default;
const cypress = require('cypress');
const {
getSpecToTest,
recordSpecResult,
updateCycle,
uploadScreenshot,
} = require('./utils/dashboard');
const {writeJsonToFile} = require('./utils/report');
const {MOCHAWESOME_REPORT_DIR, RESULTS_DIR} = require('./utils/constants');
require('dotenv').config();
const {
BRANCH,
BROWSER,
BUILD_ID,
CI_BASE_URL,
HEADLESS,
REPO,
} = process.env;
async function runCypressTest(specExecution) {
const browser = BROWSER || 'chrome';
const headless = isHeadless();
const result = await cypress.run({
browser,
headless,
spec: specExecution.file,
config: {
screenshotsFolder: `${MOCHAWESOME_REPORT_DIR}/screenshots`,
videosFolder: `${MOCHAWESOME_REPORT_DIR}/videos`,
trashAssetsBeforeRuns: false,
},
reporter: 'cypress-multi-reporters',
reporterOptions: {
reporterEnabled: 'mocha-junit-reporter, mochawesome',
mochaJunitReporterReporterOptions: {
mochaFile: 'results/junit/test_results[hash].xml',
toConsole: false,
},
mochawesomeReporterOptions: {
reportDir: MOCHAWESOME_REPORT_DIR,
reportFilename: `json/${specExecution.file}`,
quiet: true,
overwrite: false,
html: false,
json: true,
testMeta: {
browser,
headless,
branch: BRANCH,
buildId: BUILD_ID,
},
},
},
});
return result;
}
async function saveResult(specExecution, result, testIndex) {
// Write and update test environment details once
if (testIndex === 0) {
const environment = {
cypress_version: result.cypressVersion,
browser_name: result.browserName,
browser_version: result.browserVersion,
headless: isHeadless(),
os_name: result.osName,
os_version: result.osVersion,
node_version: process.version,
};
writeJsonToFile(environment, 'environment.json', RESULTS_DIR);
await updateCycle(specExecution.cycle_id, environment);
}
const {stats, tests, spec} = result.runs[0];
const specPatch = {
file: spec.relative,
tests: spec.tests,
pass: stats.passes,
fail: stats.failures,
pending: stats.pending,
skipped: stats.skipped,
duration: stats.duration || 0,
test_start_at: stats.startedAt,
test_end_at: stats.endedAt,
};
const testCases = [];
for (let i = 0; i < tests.length; i++) {
const test = tests[i];
const attempts = test.attempts.pop();
const testCase = {
title: test.title,
full_title: test.title.join(' '),
state: attempts.state,
duration: attempts.duration || 0,
code: trimToMaxLength(test.body),
};
if (attempts.startedAt) {
testCase.test_start_at = attempts.startedAt;
}
if (test.displayError) {
testCase.error_display = trimToMaxLength(test.displayError);
}
const errorFrame = attempts.error && attempts.error.codeFrame && attempts.error.codeFrame.frame;
if (errorFrame) {
testCase.error_frame = trimToMaxLength(errorFrame);
}
if (attempts.screenshots && attempts.screenshots.length > 0) {
const path = attempts.screenshots[0].path;
const screenshotUrl = await uploadScreenshot(path, REPO, BRANCH, BUILD_ID);
if (typeof screenshotUrl === 'string' && !screenshotUrl.error) {
testCase.screenshot = {url: screenshotUrl};
}
}
testCases.push(testCase);
}
await recordSpecResult(specExecution.id, specPatch, testCases);
}
function isHeadless() {
return typeof HEADLESS === 'undefined' ? true : HEADLESS === 'true';
}
function trimToMaxLength(text) {
const maxLength = 5000;
return text && text.length > maxLength ? text.substring(0, maxLength) : text;
}
function printSummary(summary) {
const obj = summary.reduce((acc, item) => {
const {server, state, count} = item;
if (!server) {
return acc;
}
if (acc[server]) {
acc[server][state] = count;
} else {
acc[server] = {[state]: count, server};
}
return acc;
}, {});
Object.values(obj).sort((a, b) => {
return a.server.localeCompare(b.server);
}).forEach((item) => {
const {server, done, started} = item;
console.log(chalk.magenta(`${server}: done: ${done || 0}, started: ${started || 0}`));
});
}
const maxRetryCount = 5;
async function runSpecFragment(count, retry) {
console.log(chalk.magenta(`Preparing for: ${count + 1}`));
const spec = await getSpecToTest({
repo: REPO,
branch: BRANCH,
build: BUILD_ID,
server: CI_BASE_URL,
});
// Retry on connection/timeout errors
if (!spec || spec.code) {
if (retry >= maxRetryCount) {
return {
tryNext: false,
count,
message: `Test ended due to multiple (${retry}) connection/timeout errors with the dashboard server.`,
};
}
console.log(chalk.red(`Retry count: ${retry}`));
return runSpecFragment(count, retry + 1);
}
if (!spec.execution || !spec.execution.file) {
return {
tryNext: false,
count,
message: spec.message,
};
}
const currentTestCount = spec.summary.reduce((total, item) => {
return total + parseInt(item.count, 10);
}, 0);
printSummary(spec.summary);
console.log(chalk.magenta(`\n(Testing ${currentTestCount} of ${spec.cycle.specs_registered}) - ${spec.execution.file}`));
console.log(chalk.magenta(`At "${process.env.CI_BASE_URL}" server`));
const result = await runCypressTest(spec.execution);
await saveResult(spec.execution, result, count);
const newCount = count + 1;
if (spec.cycle.specs_registered === currentTestCount) {
return {
tryNext: false,
count: newCount,
message: `Completed testing of all registered ${currentTestCount} spec/s.`,
};
}
return {
tryNext: true,
count: newCount,
retry: 0,
message: 'Continue testing',
};
}
async function runSpec(count = 0, retry = 0) {
const fragment = await runSpecFragment(count, retry);
if (fragment.tryNext) {
return runSpec(fragment.count, fragment.retry);
}
return {
count: fragment.count,
message: fragment.message,
};
}
runSpec().then(({count, message}) => {
console.log(chalk.magenta(message));
if (count > 0) {
console.log(chalk.magenta(`This test runner has completed ${count} spec file/s.`));
}
});

View File

@@ -0,0 +1,203 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
/*
* This command, which normally use in CI, runs Cypress test in full or partial
* depending on test metadata and environment capabilities.
*
* Usage: [ENVIRONMENT] node run_tests.js [options]
*
* Options:
* --stage=[stage]
* Selects spec files with matching stage. It can be of multiple values separated by comma.
* E.g. "--stage='@prod,@dev'" will select files with either @prod or @dev.
* --group=[group]
* Selects spec files with matching group. It can be of multiple values separated by comma.
* E.g. "--group='@channel,@messaging'" will select files with either @channel or @messaging.
* --invert
* Selected files are those not matching any of the specified stage or group.
* --include-group=[group]
* Include spec files with matching group. It can be of multiple values separated by comma.
* E.g. "--include-group='@enterprise'" will select files including @enterprise.
* --exclude-group=[group]
* Exclude spec files with matching group. It can be of multiple values separated by comma.
* E.g. "--exclude-group='@enterprise'" will select files except @enterprise.
* --include-file=[filename or directory]
* Include spec files with matching directory or filename pattern. Uses `find` command under the hood. It can be of multiple values separated by comma.
* E.g. "--include-file='channel'" will include files recursively under `channel` directory/s.
* E.g. "--include-file='*channel*'" will include files and files under directory/s recursively that matches the name with `*channel*`.
* --exclude-file=[filename or directory]
* Exclude spec files with matching directory or filename pattern. Uses `find` command under the hood. It can be of multiple values separated by comma.
* E.g. "--exclude-file='channel'" will exclude files recursively under `channel` directory/s.
* E.g. "--exclude-file='*channel*'" will exclude files and files under directory/s recursively that matches the name with `*channel*`.
* --stress-test-count=[number of repeated executions]
* The amount of times to run each spec file for. If greater than 1, report how the percentage of successes over total runs for each spec.
*
* Environment:
* BROWSER=[browser] : Chrome by default. Set to run test on other browser such as chrome, edge, electron and firefox.
* The environment should have the specified browser to successfully run.
* HEADLESS=[boolean] : Headless by default (true) or false to run on headed mode.
* BRANCH=[branch] : Branch identifier from CI
* BUILD_ID=[build_id] : Build identifier from CI
* CI_BASE_URL=[ci_base_url] : Test server base URL in CI
*
* Example:
* 1. "node run_tests.js"
* - will run all the specs on default test environment, except those matching skipped metadata
* 2. "node run_tests.js --stage='@prod'"
* - will run all production tests, except those matching skipped metadata
* 3. "node run_tests.js --stage='@prod' --invert"
* - will run all non-production tests
* 4. "BROWSER='chrome' HEADLESS='false' node run_tests.js --stage='@prod' --group='@channel,@messaging'"
* - will run spec files matching stage and group values in Chrome (headed)
* 5. "node run_tests.js --stage='@prod' --exclude-group='@enterprise'"
* - will run all production tests except @enterprise group
* - typical test run for Team Edition
* 6. "node run_tests.js --stage='@prod' --part=1 --of=2"
* - will run the first half (1 of 2) of all production tests
* - will be used for parallel testing where each part could run separately against its own test server
*/
const os = require('os');
const chalk = require('chalk').default;
const cypress = require('cypress');
const argv = require('yargs')(process.argv.slice(2)).argv;
const {getSortedTestFiles} = require('./utils/file');
const {getTestFilesIdentifier} = require('./utils/even_distribution');
const {writeJsonToFile} = require('./utils/report');
const {MOCHAWESOME_REPORT_DIR, RESULTS_DIR} = require('./utils/constants');
require('dotenv').config();
async function runTests() {
const {
BRANCH,
BROWSER,
BUILD_ID,
HEADLESS,
} = process.env;
const browser = BROWSER || 'chrome';
const headless = typeof HEADLESS === 'undefined' ? true : HEADLESS === 'true';
const platform = os.platform();
const stressTestCount = argv.stressTestCount > 1 ? argv.stressTestCount : 1;
const testPasses = {};
const sortedFilesObject = getSortedTestFiles(platform, browser, headless);
const sortedFiles = sortedFilesObject.sortedFiles.flatMap((el) => Array(stressTestCount).fill(el));
const numberOfTestFiles = sortedFiles.length;
if (!numberOfTestFiles) {
console.log(chalk.red('Nothing to test!'));
return;
}
const {
start,
end,
count,
} = getTestFilesIdentifier(numberOfTestFiles, argv.part, argv.of);
for (let i = start, j = 0; i < end && j < count; i++, j++) {
printMessage(sortedFiles, i, j + 1, count);
const testFile = sortedFiles[i];
let testFileAttempt = 1;
if (testFile in testPasses === false) {
testPasses[testFile] = {attempt: 1};
} else {
testPasses[testFile].attempt += 1;
testFileAttempt = testPasses[testFile].attempt;
}
const result = await cypress.run({
browser,
headless,
spec: testFile,
config: {
screenshotsFolder: `${MOCHAWESOME_REPORT_DIR}/screenshots`,
videosFolder: `${MOCHAWESOME_REPORT_DIR}/videos`,
trashAssetsBeforeRuns: false,
retries: stressTestCount > 1 ? 0 : 2,
},
env: {
firstTest: j === 0,
},
reporter: 'cypress-multi-reporters',
reporterOptions: {
reporterEnabled: 'mocha-junit-reporter, mochawesome',
mochaJunitReporterReporterOptions: {
mochaFile: 'results/junit/test_results[hash].xml',
toConsole: false,
},
mochawesomeReporterOptions: {
reportDir: MOCHAWESOME_REPORT_DIR,
reportFilename: `json/${testFile}`,
quiet: true,
overwrite: false,
html: false,
json: true,
testMeta: {
platform,
browser,
headless,
branch: BRANCH,
buildId: BUILD_ID,
testFileAttempt,
},
},
},
});
for (const testCase of result.runs[0].tests) {
const testCaseTitle = testCase.title.join(' - ');
if (testCaseTitle in testPasses[testFile] === false) {
testPasses[testFile][testCaseTitle] = 0;
}
if (testCase.state === 'passed') {
testPasses[testFile][testCaseTitle] += 1;
}
}
// Write test environment details once only
if (i === 0) {
const environment = {
cypress_version: result.cypressVersion,
browser_name: result.browserName,
browser_version: result.browserVersion,
headless,
os_name: result.osName,
os_version: result.osVersion,
node_version: process.version,
};
writeJsonToFile(environment, 'environment.json', RESULTS_DIR);
}
}
writeJsonToFile(testPasses, 'testPasses.json', RESULTS_DIR);
}
function printMessage(testFiles, overallIndex, currentItem, lastItem) {
const {invert, excludeGroup, group, stage} = argv;
const testFile = testFiles[overallIndex];
const testStage = stage ? `Stage: "${stage}" ` : '';
const withGroup = group || excludeGroup;
const groupMessage = group ? `"${group}"` : 'All';
const excludeGroupMessage = excludeGroup ? `except "${excludeGroup}"` : '';
const testGroup = withGroup ? `Group: ${groupMessage} ${excludeGroupMessage}` : '';
// Log which files were being tested
console.log(chalk.magenta.bold(`${invert ? 'All Except --> ' : ''}${testStage}${stage && withGroup ? '| ' : ''}${testGroup}`));
console.log(chalk.magenta(`(Testing ${overallIndex + 1} of ${testFiles.length}) - `, testFile));
if (process.env.CI_BASE_URL) {
console.log(chalk.magenta(`Testing ${currentItem}/${lastItem} in "${process.env.CI_BASE_URL}" server`));
}
}
runTests();

View File

@@ -0,0 +1,109 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
/*
* This is used for sending data to automation dashboard and
* publishing quick summary to community channels.
*
* Usage: [ENV] node save_report.js
*
* Environment variables:
* BRANCH=[branch] : Branch identifier from CI
* BUILD_ID=[build_id] : Build identifier from CI
* BUILD_TAG=[build_tag] : Docker image used to run the test
*
* For saving test cases to Test Management
* - TM4J_ENABLE=true|false
* - TM4J_API_KEY=[api_key]
* - JIRA_PROJECT_KEY=[project_key], e.g. "MM",
* - TM4J_FOLDER_ID=[folder_id], e.g. 847997
* For sending hooks to Mattermost channels
* - FULL_REPORT, WEBHOOK_URL and DIAGNOSTIC_WEBHOOK_URL
* - If AUTOMATION_DASHBOARD_FRONTEND_URL is set, a link to the corresponding cycle will be included in the report message
* Test type
* - TYPE=[type]; valid values: "PR", "RELEASE", "MASTER", "MASTER_UNSTABLE", "CLOUD", "CLOUD_UNSTABLE", "NONE"
* Server type
* - SERVER_TYPE=[type]; used for the 'Test Server' field in the webhook. Any string representing the server type is valid, common values are "onprem" and "cloud"
*/
const {merge} = require('mochawesome-merge');
const generator = require('mochawesome-report-generator');
const {
generateDiagnosticReport,
generateShortSummary,
generateTestReport,
removeOldGeneratedReports,
sendReport,
readJsonFromFile,
writeJsonToFile,
} = require('./utils/report');
const {MOCHAWESOME_REPORT_DIR, RESULTS_DIR} = require('./utils/constants');
const {createTestCycle, createTestExecutions} = require('./utils/test_cases');
require('dotenv').config();
const saveReport = async () => {
const {
BRANCH,
BUILD_ID,
BUILD_TAG,
DIAGNOSTIC_WEBHOOK_URL,
DIAGNOSTIC_USER_ID,
DIAGNOSTIC_TEAM_ID,
TM4J_ENABLE,
TM4J_CYCLE_KEY,
TYPE,
WEBHOOK_URL,
} = process.env;
removeOldGeneratedReports();
// Merge all json reports into one single json report
const jsonReport = await merge({files: [`${MOCHAWESOME_REPORT_DIR}/**/*.json`]});
writeJsonToFile(jsonReport, 'all.json', MOCHAWESOME_REPORT_DIR);
// Generate the html report file
await generator.create(
jsonReport,
{
reportDir: MOCHAWESOME_REPORT_DIR,
reportTitle: `Build:${BUILD_ID} Branch: ${BRANCH} Tag: ${BUILD_TAG}`,
},
);
// Generate short summary, write to file and then send report via webhook
const summary = generateShortSummary(jsonReport);
console.log(summary);
writeJsonToFile(summary, 'summary.json', MOCHAWESOME_REPORT_DIR);
// Create or use an existing test cycle
let testCycle = {};
if (TM4J_ENABLE === 'true') {
const {start, end} = jsonReport.stats;
testCycle = TM4J_CYCLE_KEY ? {key: TM4J_CYCLE_KEY} : await createTestCycle(start, end);
}
// Send test report to "QA: UI Test Automation" channel via webhook
if (TYPE && TYPE !== 'NONE' && WEBHOOK_URL) {
const environment = readJsonFromFile(`${RESULTS_DIR}/environment.json`);
const data = generateTestReport(summary, false, undefined, environment, testCycle.key);
await sendReport('summary report to Community channel', WEBHOOK_URL, data);
}
// Send diagnostic report via webhook
// Send on "RELEASE" type only
if (TYPE === 'RELEASE' && DIAGNOSTIC_WEBHOOK_URL && DIAGNOSTIC_USER_ID && DIAGNOSTIC_TEAM_ID) {
const data = generateDiagnosticReport(summary, {userId: DIAGNOSTIC_USER_ID, teamId: DIAGNOSTIC_TEAM_ID});
await sendReport('test info for diagnostic analysis', DIAGNOSTIC_WEBHOOK_URL, data);
}
// Save test cases to Test Management
if (TM4J_ENABLE === 'true') {
await createTestExecutions(jsonReport, testCycle);
}
};
saveReport();

View File

@@ -0,0 +1,21 @@
/* eslint-disable headers/header-format */
// taken from https://github.com/guilryder/chrome-extensions/tree/master/xframe_ignore
/*global chrome*/
const HEADERS_TO_STRIP_LOWERCASE = [
'content-security-policy',
'x-frame-options',
];
chrome.webRequest.onHeadersReceived.addListener(
(details) => {
return {
responseHeaders: details.responseHeaders.filter((header) => {
return HEADERS_TO_STRIP_LOWERCASE.indexOf(header.name.toLowerCase()) < 0;
}),
};
}, {
urls: ['<all_urls>'],
}, ['blocking', 'responseHeaders']);

View File

@@ -0,0 +1,17 @@
{
"update_url": "https://clients2.google.com/service/update2/crx",
"manifest_version": 2,
"name": "Ignore X-Frame headers",
"description": "Drops X-Frame-Options and Content-Security-Policy HTTP response headers, allowing all pages to be iframed.",
"version": "1.1",
"background": {
"scripts": [
"background.js"
]
},
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,22 @@
{
"mastercard":{
"cardNumber":"5555555555554444",
"expDate":"4242",
"cvc":"412"
},
"visa":{
"cardNumber":"4242424242424242",
"expDate":"4242",
"cvc":"412"
},
"unionpay":{
"cardNumber":"6200000000000005",
"expDate":"1244",
"cvc":"123"
},
"invalidvisa":{
"cardNumber":"4242424242424141",
"expDate":"1212",
"cvc":"12"
}
}

View File

@@ -0,0 +1,416 @@
[
{
"section": "about.license",
"disabledInputs": [
{
"path": "/admin_console/about/license",
"selector": "remove-button"
}
]
},
{
"section": "reporting.system_analytics",
"disabledInputs": []
},
{
"section": "reporting.team_statistics",
"disabledInputs": []
},
{
"section": "reporting.server_logs",
"disabledInputs": []
},
{
"section": "user_management.system_users",
"disabledInputs": []
},
{
"section": "user_management.groups",
"disabledInputs": []
},
{
"section": "user_management.teams",
"disabledInputs": []
},
{
"section": "user_management.channel",
"disabledInputs": []
},
{
"section": "user_management.permissions",
"disabledInputs": []
},
{
"section": "environment.web_server",
"disabledInputs": [
{
"path": "/admin_console/environment/web_server",
"selector": "ServiceSettings.ListenAddressinput"
}
]
},
{
"section": "site.customization",
"disabledInputs": [
{
"path": "admin_console/site_config/customization",
"selector": "TeamSettings.SiteNameinput"
}
]
},
{
"section": "site.localization",
"disabledInputs": [
{
"path": "admin_console/site_config/localization",
"selector": "LocalizationSettings.DefaultServerLocaledropdown"
}
]
},
{
"section": "site.users_and_teams",
"disabledInputs": [
{
"path": "admin_console/site_config/users_and_teams",
"selector": "TeamSettings.MaxUsersPerTeamnumber"
}
]
},
{
"section": "site.notifications",
"disabledInputs": [
{
"path": "admin_console/environment/notifications",
"selector": "TeamSettings.EnableConfirmNotificationsToChanneltrue"
}
]
},
{
"section": "site.announcement_banner",
"disabledInputs": [
{
"path": "admin_console/site_config/announcement_banner",
"selector": "AnnouncementSettings.EnableBannertrue"
}
]
},
{
"section": "site.emoji",
"disabledInputs": [
{
"path": "admin_console/site_config/emoji",
"selector": "ServiceSettings.EnableEmojiPickertrue"
}
]
},
{
"section": "site.posts",
"disabledInputs": [
{
"path": "admin_console/site_config/posts",
"selector": "ServiceSettings.EnableLinkPreviewstrue"
}
]
},
{
"section": "site.file_sharing_downloads",
"disabledInputs": [
{
"path": "admin_console/site_config/file_sharing_downloads",
"selector": "FileSettings.EnableFileAttachmentstrue"
}
]
},
{
"section": "site.public_links",
"disabledInputs": [
{
"path": "admin_console/site_config/public_links",
"selector": "FileSettings.EnablePublicLinktrue"
}
]
},
{
"section": "site.notices",
"disabledInputs": [
{
"path": "admin_console/site_config/notices",
"selector": "AnnouncementSettings.AdminNoticesEnabledtrue"
}
]
},
{
"section": "environment.database",
"disabledInputs": [
{
"path": "/admin_console/environment/database",
"selector": "maxIdleConnsinput"
}
]
},
{
"section": "environment.elasticsearch",
"disabledInputs": [
{
"path": "/admin_console/environment/elasticsearch",
"selector": "enableIndexingtrue"
}
]
},
{
"section": "environment.storage",
"disabledInputs": [
{
"path": "/admin_console/environment/file_storage",
"selector": "FileSettings.DriverNamedropdown"
}
]
},
{
"section": "environment.image_proxy",
"disabledInputs": [
{
"path": "/admin_console/environment/image_proxy",
"selector": "ImageProxySettings.Enabletrue"
}
]
},
{
"section": "environment.smtp",
"disabledInputs": [
{
"path": "/admin_console/environment/smtp",
"selector": "EmailSettings.EnableSMTPAuthtrue"
}
]
},
{
"section": "environment.push_notification_server",
"disabledInputs": [
{
"path": "/admin_console/environment/push_notification_server",
"selector": "pushNotificationServerTypedropdown"
}
]
},
{
"section": "environment.high_availability",
"disabledInputs": [
{
"path": "/admin_console/environment/high_availability",
"selector": "Enabletrue"
}
]
},
{
"section": "environment.rate_limiting",
"disabledInputs": [
{
"path": "/admin_console/environment/rate_limiting",
"selector": "RateLimitSettings.Enabletrue"
}
]
},
{
"section": "environment.logging",
"disabledInputs": [
{
"path": "/admin_console/environment/logging",
"selector": "LogSettings.ConsoleLeveldropdown"
}
]
},
{
"section": "environment.session_lengths",
"disabledInputs": [
{
"path": "/admin_console/environment/session_lengths",
"selector": "sessionLengthWebInDaysinput"
}
]
},
{
"section": "environment.metrics",
"disabledInputs": [
{
"path": "/admin_console/environment/performance_monitoring",
"selector": "MetricsSettings.ListenAddressinput"
}
]
},
{
"section": "environment.developer",
"disabledInputs": [
{
"path": "/admin_console/environment/developer",
"selector": "ServiceSettings.EnableTestingtrue"
}
]
},
{
"section": "authentication.signup",
"disabledInputs":[
{
"path": "/admin_console/authentication/signup",
"selector": "TeamSettings.EnableUserCreationfalse"
}
]
},
{
"section": "authentication.email",
"disabledInputs": [
{
"path": "/admin_console/authentication/email",
"selector": "EmailSettings.EnableSignUpWithEmailfalse"
}
]
},
{
"section": "authentication.password",
"disabledInputs": [
{
"path": "/admin_console/authentication/password",
"selector": "passwordMinimumLengthinput"
}
]
},
{
"section": "authentication.mfa",
"disabledInputs": [
{
"path": "/admin_console/authentication/mfa",
"selector": "ServiceSettings.EnableMultifactorAuthenticationfalse"
}
]
},
{
"section": "authentication.ldap",
"disabledInputs": [
{
"path": "/admin_console/authentication/ldap",
"selector": "LdapSettings.Enablefalse"
}
]
},
{
"section": "authentication.saml",
"disabledInputs": [
{
"path": "/admin_console/authentication/saml",
"selector": "SamlSettings.Enablefalse"
}
]
},
{
"section": "authentication.openid",
"disabledInputs": [
{
"path": "/admin_console/authentication/openid",
"selector": "openidTypedropdown"
}
]
},
{
"section": "authentication.guest_access",
"disabledInputs": [
{
"path": "/admin_console/authentication/guest_access",
"selector": "GuestAccountsSettings.Enablefalse"
}
]
},
{
"section": "plugins",
"disabledInputs": [
{
"path": "/admin_console/plugins/plugin_management",
"selector": "marketplaceUrlinput"
}
]
},
{
"section": "integrations.integration_management",
"disabledInputs": [
{
"path": "/admin_console/integrations/integration_management",
"selector": "ServiceSettings.EnableIncomingWebhookstrue"
}
]
},
{
"section": "integrations.bot_accounts",
"disabledInputs": [
{
"path": "/admin_console/integrations/bot_accounts",
"selector": "ServiceSettings.EnableBotAccountCreationtrue"
}
]
},
{
"section": "integrations.gif",
"disabledInputs": [
{
"path": "/admin_console/integrations/gif",
"selector": "ServiceSettings.EnableGifPickertrue"
}
]
},
{
"section": "integrations.cors",
"disabledInputs": [
{
"path": "/admin_console/integrations/cors",
"selector": "ServiceSettings.AllowCorsFrominput"
}
]
},
{
"section": "compliance.data_retention",
"disabledInputs": []
},
{
"section": "compliance.message_export",
"disabledInputs": [
{
"path": "/admin_console/compliance/export",
"selector": "enableComplianceExporttrue"
}
]
},
{
"section": "compliance.audits",
"disabledInputs": [
{
"path": "/admin_console/compliance/monitoring",
"selector": "ComplianceSettings.Enabletrue"
}
]
},
{
"section": "compliance.custom_terms_of_service",
"disabledInputs": [
{
"path": "/admin_console/compliance/custom_terms_of_service",
"selector": "SupportSettings.CustomTermsOfServiceEnabledtrue"
}
]
},
{
"section": "experimental.experimental_features",
"disabledInputs": [
{
"path": "/admin_console/experimental/features",
"selector": "ExperimentalSettings.LinkMetadataTimeoutMillisecondsnumber"
}
]
},
{
"section": "experimental.feature_flags",
"disabledInputs": [
{
"path": "/admin_console/experimental/feature_flags",
"selector": ""
}
]
}
]

View File

@@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
module.exports = {
TIME_12_HOUR: 'h:mm A', // no leading zeros
TIME_24_HOUR: 'HH:mm', // with leading zeros
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -0,0 +1,26 @@
{
"attachments": [{
"pretext": "This is the attachment pretext.",
"text": "This is the attachment text.",
"actions": [{
"name": "Select an option...",
"integration": {
"url": "http://localhost:3000/message_menus",
"context": {
"action": "do_something"
}
},
"type": "select",
"options": [{
"text": "Option 1",
"value": "option1"
}, {
"text": "Option 2",
"value": "option2"
}, {
"text": "Option 3",
"value": "option3"
}]
}]
}]
}

View File

@@ -0,0 +1,17 @@
{
"attachments": [{
"pretext": "This is the attachment pretext.",
"text": "This is the attachment text.",
"actions": [{
"name": "Select an option...",
"integration": {
"url": "http://localhost:3000/message_menus_datasource",
"context": {
"action": "do_something"
}
},
"type": "select",
"data_source": "channels"
}]
}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,72 @@
{
"many-options": [
{"text": "Afghanistan", "value": "AF"},
{"text": "Åland Islands", "value": "AX"},
{"text": "Albania", "value": "AL"},
{"text": "Algeria", "value": "DZ"},
{"text": "American Samoa", "value": "AS"},
{"text": "AndorrA", "value": "AD"},
{"text": "Angola", "value": "AO"},
{"text": "Anguilla", "value": "AI"},
{"text": "Antarctica", "value": "AQ"},
{"text": "Antigua and Barbuda", "value": "AG"},
{"text": "Argentina", "value": "AR"},
{"text": "Armenia", "value": "AM"},
{"text": "Aruba", "value": "AW"},
{"text": "Australia", "value": "AU"},
{"text": "Austria", "value": "AT"},
{"text": "Azerbaijan", "value": "AZ"},
{"text": "Bahamas", "value": "BS"},
{"text": "Bahrain", "value": "BH"},
{"text": "Bangladesh", "value": "BD"},
{"text": "Barbados", "value": "BB"},
{"text": "Belarus", "value": "BY"},
{"text": "Belgium", "value": "BE"},
{"text": "Belize", "value": "BZ"},
{"text": "Benin", "value": "BJ"},
{"text": "Bermuda", "value": "BM"},
{"text": "Bhutan", "value": "BT"},
{"text": "Bolivia", "value": "BO"},
{"text": "Bosnia and Herzegovina", "value": "BA"},
{"text": "Botswana", "value": "BW"},
{"text": "Bouvet Island", "value": "BV"},
{"text": "Brazil", "value": "BR"},
{"text": "British Indian Ocean Territory", "value": "IO"},
{"text": "Brunei Darussalam", "value": "BN"},
{"text": "Bulgaria", "value": "BG"},
{"text": "Burkina Faso", "value": "BF"},
{"text": "Burundi", "value": "BI"},
{"text": "Cambodia", "value": "KH"},
{"text": "Cameroon", "value": "CM"},
{"text": "Canada", "value": "CA"},
{"text": "Cape Verde", "value": "CV"},
{"text": "Cayman Islands", "value": "KY"},
{"text": "Central African Republic", "value": "CF"},
{"text": "Chad", "value": "TD"},
{"text": "Chile", "value": "CL"},
{"text": "China", "value": "CN"},
{"text": "Christmas Island", "value": "CX"},
{"text": "Cocos (Keeling) Islands", "value": "CC"},
{"text": "Colombia", "value": "CO"},
{"text": "Comoros", "value": "KM"},
{"text": "Congo", "value": "CG"},
{"text": "Congo, The Democratic Republic of the", "value": "CD"},
{"text": "Cook Islands", "value": "CK"},
{"text": "Costa Rica", "value": "CR"},
{"text": "Cote D\"Ivoire", "value": "CI"},
{"text": "Croatia", "value": "HR"},
{"text": "Cuba", "value": "CU"},
{"text": "Cyprus", "value": "CY"},
{"text": "Czech Republic", "value": "CZ"}
],
"distinct-options": [
{"text": "Apple", "value": "apple"},
{"text": "Orange", "value": "orange"},
{"text": "Banana", "value": "banana"},
{"text": "Grapes", "value": "grapes"},
{"text": "Melon", "value": "melon"},
{"text": "Mango", "value": "mango"},
{"text": "Mango Raw", "value": "mangoraw"},
{"text": "Avocado", "value": "avocado"}
]
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -0,0 +1,8 @@
dn: uid=e2etest.four,ou=e2etest,dc=mm,dc=test,dc=com
changetype: add
objectclass: iNetOrgPerson
sn: FourLDAP
cn: TestLDAP
uid: e2etest.four
mail: e2etest.four@mmtest.com
userPassword: Password1

View File

@@ -0,0 +1,43 @@
dn: uid=e2etest.one,ou=e2etest,dc=mm,dc=test,dc=com
changetype: delete
dn: uid=e2etest.two,ou=e2etest,dc=mm,dc=test,dc=com
changetype: delete
dn: uid=e2etest.three,ou=e2etest,dc=mm,dc=test,dc=com
changetype: delete
dn: uid=e2etest.four,ou=e2etest,dc=mm,dc=test,dc=com
changetype: delete
dn: ou=e2etest,dc=mm,dc=test,dc=com
changetype: add
objectclass: organizationalunit
# generic test users
dn: uid=e2etest.one,ou=e2etest,dc=mm,dc=test,dc=com
changetype: add
objectclass: iNetOrgPerson
sn: OneLDAP
cn: TestLDAP
uid: e2etest.one
mail: e2etest.one@mmtest.com
userPassword: Password1
dn: uid=e2etest.two,ou=e2etest,dc=mm,dc=test,dc=com
changetype: add
objectclass: iNetOrgPerson
sn: TwoLDAP
cn: TestLDAP
uid: e2etest.two
mail: e2etest.two@mmtest.com
userPassword: Password1
dn: uid=e2etest.three,ou=e2etest,dc=mm,dc=test,dc=com
changetype: add
objectclass: iNetOrgPerson
sn: ThreeLDAP
cn: TestLDAP
uid: e2etest.three.ldap
mail: e2etest.three@mmtest.com
userPassword: Password1

View File

@@ -0,0 +1,44 @@
{
"dev-1": {
"username": "dev.one",
"password": "Password1",
"email": "success+devone@simulator.amazonses.com",
"userType": "Admin"
},
"dev-2": {
"username": "dev.two",
"password": "Password1",
"email": "success+devtwo@simulator.amazonses.com",
"userType": "Admin"
},
"test-1": {
"username": "test.one",
"password": "Password1",
"email": "success+testone@simulator.amazonses.com",
"userType": ""
},
"test-2": {
"username": "test.two",
"password": "Password1",
"email": "success+testtwo@simulator.amazonses.com",
"userType": ""
},
"test-3": {
"username": "test.three",
"password": "Password1",
"email": "success+testthree@simulator.amazonses.com",
"userType": ""
},
"board-1": {
"username": "board.one",
"password": "Password1",
"email": "success+boardone@simulator.amazonses.com",
"userType": ""
},
"board-2": {
"username": "board.two",
"password": "Password1",
"email": "success+boardtwo@simulator.amazonses.com",
"userType": ""
}
}

View File

@@ -0,0 +1,2 @@
The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Bright vixens jump; dozy fowl quack. Quick wafting zephyrs vex bold Jim. Quick zephyrs blow, vexing daft Jim. Sex-charged fop blew my junk TV quiz. How quickly daft jumping zebras vex. Two driven jocks help fax my big quiz. Quick, Baz, get my woven flax jodhpurs! "Now fax quiz Jack!" my brave ghost pled. Five quacking zephyrs jolt my wax bed. Flummoxed by job, kvetching W. zaps Iraq. Cozy sphinx waves quart jug of bad milk. A very bad quack might jinx zippy fowls. Few quips galvanized the mock jury box. Quick brown dogs jump over the lazy fox. The jay, pig, fox, zebra, and my wolves quack! Blowzy red vixens fight for a quick jump. Joaquin Phoenix was gazed by MTV for luck. A wizard’s job is to vex chumps quickly in fog. Watch "Jeopardy!", Alex Trebek's fun TV quiz game. Woven silk pyjamas exchanged for blue quartz. Brawny gods just flocked up to quiz and vex him. Adjusting quiver and bow, Zompyc[1] killed the fox. My faxed joke won a pager in the cable TV quiz show. Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid jackets before she quit. Six big devils from Japan quickly forgot how to waltz. Big July earthquakes confound zany experimental vow. Foxy parsons quiz and cajole the lovably dim wiki-girl. Have a pick: twenty six letters - no forcing a jumbled quiz! Crazy Fredericka bought many very exquisite opal jewels. Sixty zippers were quickly picked from the woven jute bag. A quick movement of the enemy will jeopardize six gunboats. All questions asked by five watch experts amazed the judge. Jack quietly moved up front and seized the big ball of wax. The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Bright vixens jump; dozy fowl quack. Quick wafting zephyrs vex bold Jim. Quick zephyrs blow, vexing daft Jim. Sex-charged fop blew my junk TV quiz. How quickly daft jumping zebras vex. Two driven jocks help fax my big quiz. Quick, Baz, get my woven flax jodhpurs! "Now fax quiz Jack!" my brave ghost pled. Five quacking zephyrs jolt my wax bed. Flummoxed by job, kvetching W. zaps Iraq. Cozy sphinx waves quart jug of bad milk. A very bad quack might jinx zippy fowls. Few quips galvanized the mock jury box. Quick brown dogs jump over the lazy fox. The jay, pig, fox, zebra, and my wolves quack! Blowzy red vixens fight for a quick jump. Joaquin Phoenix was gazed by MTV for luck. A wizard’s job is to vex chumps quickly in fog. Watch "Jeopardy!", Alex Trebek's fun TV quiz game. Woven silk pyjamas exchanged for blue quartz. Brawny gods just flocked up to quiz and vex him. Adjusting quiver and bow, Zompyc[1] killed the fox. My faxed joke won a pager in the cable TV quiz show. Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid jackets before she quit. Six big devils from Japan quickly forgot how to waltz. Big July earthquakes confound zany experimental vow. Foxy parsons quiz and cajole the lovably dim wiki-girl. Have a pick: twenty six letters - no forcing a jumbled quiz! Crazy Fredericka bought many very exquisite opal jewels. Sixty zippers were quickly picked from the woven jute bag. A quick movement of the enemy will jeopardize six gunboats. All questions asked by five watch experts amazed the judge. Jack quietly moved up front and seized the big ball of wax. The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Hello this is a long post, with more than 4000 characters, plus multiple attachments.
The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Bright vixens jump; dozy fowl quack. Quick wafting zephyrs vex bold Jim. Quick zephyrs blow, vexing daft Jim. Sex-charged fop blew my junk TV quiz. How quickly daft jumping zebras vex. Two driven jocks help fax my big quiz. Quick, Baz, get my woven flax jodhpurs! "Now fax quiz Jack!" my brave ghost pled. Five quacking zephyrs jolt my wax bed. Flummoxed by job, kvetching W. zaps Iraq. Cozy sphinx waves quart jug of bad milk. A very bad quack might jinx zippy fowls. Few quips galvanized the mock jury box. Quick brown dogs jump over the lazy fox. The jay, pig, fox, zebra, and my wolves quack! Blowzy red vixens fight for a quick jump. Joaquin Phoenix was gazed by MTV for luck. A wizard’s job is to vex chumps quickly in fog. Watch "Jeopardy!", Alex Trebek's fun TV quiz game. Woven silk pyjamas exchanged for blue quartz. Brawny gods just flocked up to quiz and vex him. Adjusting quiver and bow, Zompyc[1] killed the fox. My faxed joke won a pager in the cable TV quiz show. Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid jackets before she quit. Six big devils from Japan quickly forgot how to waltz. Big July earthquakes confound zany experimental vow. Foxy parsons quiz and cajole the lovably dim wiki-girl. Have a pick: twenty six letters - no forcing a jumbled quiz! Crazy Fredericka bought many very exquisite opal jewels. Sixty zippers were quickly picked from the woven jute bag. A quick movement of the enemy will jeopardize six gunboats. All questions asked by five watch experts amazed the judge. Jack quietly moved up front and seized the big ball of wax. The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Bright vixens jump; dozy fowl quack. Quick wafting zephyrs vex bold Jim. Quick zephyrs blow, vexing daft Jim. Sex-charged fop blew my junk TV quiz. How quickly daft jumping zebras vex. Two driven jocks help fax my big quiz. Quick, Baz, get my woven flax jodhpurs! "Now fax quiz Jack!" my brave ghost pled. Five quacking zephyrs jolt my wax bed. Flummoxed by job, kvetching W. zaps Iraq. Cozy sphinx waves quart jug of bad milk. A very bad quack might jinx zippy fowls. Few quips galvanized the mock jury box. Quick brown dogs jump over the lazy fox. The jay, pig, fox, zebra, and my wolves quack! Blowzy red vixens fight for a quick jump. Joaquin Phoenix was gazed by MTV for luck. A wizard’s job is to vex chumps quickly in fog. Watch "Jeopardy!", Alex Trebek's fun TV quiz game. Woven silk pyjamas exchanged for blue quartz. Brawny gods just flocked up to quiz and vex him. Adjusting quiver and bow, Zompyc[1] killed the fox. My faxed joke won a pager in the cable TV quiz show. Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid jackets before she quit. Six big devils from Japan quickly forgot how to waltz. Big July earthquakes confound zany experimental vow. Foxy parsons quiz and cajole the lovably dim wiki-girl. Have a pick: twenty six letters - no forcing a jumbled quiz! Crazy Fredericka bought many very exquisite opal jewels. Sixty zippers were quickly picked from the woven jute bag. A quick movement of the enemy will jeopardize six gunboats. All questions asked by five watch experts amazed the judge. Jack quietly moved up front and seized the big ball of wax. The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Hello this is a long post, with more than 4000 characters, plus multiple attachments.

Binary file not shown.

View File

@@ -0,0 +1 @@
<h1 class="markdown__heading">Basic Markdown Testing</h1><p>Tests for text style, code blocks, in-line code and images, lines, block quotes, and headings.</p>

View File

@@ -0,0 +1,2 @@
# Basic Markdown Testing
Tests for text style, code blocks, in-line code and images, lines, block quotes, and headings.

View File

@@ -0,0 +1,11 @@
<h3 class="markdown__heading">Block Quotes</h3><blockquote>
<p>This text should render in a block quote.</p>
</blockquote>
<p><strong>The following text should render in two block quotes separated by one line of text:</strong></p>
<blockquote>
<p>Block quote 1</p>
</blockquote>
<p>Text between block quotes</p>
<blockquote>
<p>Block quote 2</p>
</blockquote>

View File

@@ -0,0 +1,10 @@
### Block Quotes
>This text should render in a block quote.
**The following text should render in two block quotes separated by one line of text:**
> Block quote 1
Text between block quotes
> Block quote 2

View File

@@ -0,0 +1,6 @@
### Block Quotes
**The following markdown should render within the block quote:**
> #### Heading 4
> _Italics_, *Italics*, **Bold**, ***Bold-italics***, **_Bold-italics_**, ~~Strikethrough~~
> :) :-) ;) :-O :bamboo: :gift_heart: :dolls:

View File

@@ -0,0 +1,4 @@
<h3 class="markdown__heading">Carriage Return</h3><p>Line #1 followed by one blank line</p>
<p>Line #2 followed by one blank line</p>
<p>Line #3 followed by Line #4
Line #4</p>

View File

@@ -0,0 +1,8 @@
### Carriage Return
Line #1 followed by one blank line
Line #2 followed by one blank line
Line #3 followed by Line #4
Line #4

View File

@@ -0,0 +1,4 @@
<p><strong>The following should appear as a carriage return separating two lines of text:</strong></p>
<div class="post-code post-code--wrap"><div class="post-code__overlay"><span class="post-code__clipboard"><i role="button" class="icon icon-content-copy"></i></span></div><div class="hljs"><code>Line #1 followed by a blank line
Line #2 following a blank line</code></div></div>

View File

@@ -0,0 +1,6 @@
**The following should appear as a carriage return separating two lines of text:**
```
Line #1 followed by a blank line
Line #2 following a blank line
```

View File

@@ -0,0 +1 @@
<h3 class="markdown__heading">Code Blocks</h3><div class="post-code post-code--wrap"><div class="post-code__overlay"><span class="post-code__clipboard" aria-label="Copy code" role="button"><i class="icon icon-content-copy"></i></span></div><div class="hljs"><code>This text should render in a code block</code></div></div>

View File

@@ -0,0 +1,5 @@
### Code Blocks
```
This text should render in a code block
```

View File

@@ -0,0 +1,116 @@
<h1 class="markdown__heading">Code Syntax Highlighting</h1><p>Verify the following code blocks render as code blocks and highlight properly. </p>
<h3 class="markdown__heading">Diff</h3><div class="post-code"><span class="post-code__language">Diff</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7
8
9
10</div><code><span class="hljs-comment">*** /path/to/original ''timestamp''</span>
<span class="hljs-comment">--- /path/to/new ''timestamp''</span>
<span class="hljs-comment">***************</span>
<span class="hljs-comment">*** 1 ****</span>
<span class="hljs-addition">! This is a line.</span>
<span class="hljs-comment">--- 1 ---</span>
<span class="hljs-addition">! This is a replacement line.</span>
It is important to spell
<span class="hljs-deletion">-removed line</span>
<span class="hljs-addition">+new line</span></code></div></div><h3 class="markdown__heading">Makefile</h3><div class="post-code"><span class="post-code__language">Makefile</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5</div><code>CC=gcc
CFLAGS=-I.
<span class="hljs-section">hellomake: hellomake.o hellofunc.o</span>
<span class="hljs-variable">$(CC)</span> -o hellomake hellomake.o hellofunc.o -I.</code></div></div><h3 class="markdown__heading">JSON</h3><div class="post-code"><span class="post-code__language">JSON</span><div class="hljs"><div class="post-code__line-numbers">1
2
3</div><code>{<span class="hljs-attr">"employees"</span>:[
{<span class="hljs-attr">"firstName"</span>:<span class="hljs-string">"John"</span>, <span class="hljs-attr">"lastName"</span>:<span class="hljs-string">"Doe"</span>},
]}</code></div></div><h3 class="markdown__heading">Markdown</h3><div class="post-code"><span class="post-code__language">Markdown</span><div class="hljs"><div class="post-code__line-numbers">1
2
3</div><code><span class="hljs-strong">**bold**</span>
<span class="hljs-emphasis">*italics*</span>
[<span class="hljs-string">link</span>](<span class="hljs-link">www.example.com</span>)</code></div></div><h3 class="markdown__heading">JavaScript</h3><div class="post-code"><span class="post-code__language">JavaScript</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-built_in">document</span>.write(<span class="hljs-string">'Hello, world!'</span>);</code></div></div><h3 class="markdown__heading">CSS</h3><div class="post-code"><span class="post-code__language">CSS</span><div class="hljs"><div class="post-code__line-numbers">1
2
3</div><code><span class="hljs-selector-tag">body</span> {
<span class="hljs-attribute">background-color</span>: red;
}</code></div></div><h3 class="markdown__heading">Objective C</h3><div class="post-code"><span class="post-code__language">Objective C</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6</div><code><span class="hljs-meta">#import <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
<span class="hljs-keyword">int</span> main (<span class="hljs-keyword">void</span>)
{
printf (<span class="hljs-string">"Hello world!\n"</span>);
}</code></div></div><h3 class="markdown__heading">Python</h3><div class="post-code"><span class="post-code__language">Python</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-built_in">print</span> <span class="hljs-string">"Hello, world!"</span></code></div></div><h3 class="markdown__heading">XML</h3><div class="post-code"><span class="post-code__language">HTML, XML</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5</div><code><span class="hljs-tag">&lt;<span class="hljs-name">employees</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">employee</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">firstName</span>&gt;</span>John<span class="hljs-tag">&lt;/<span class="hljs-name">firstName</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">lastName</span>&gt;</span>Doe<span class="hljs-tag">&lt;/<span class="hljs-name">lastName</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">employee</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">employees</span>&gt;</span></code></div></div><h3 class="markdown__heading">Perl</h3><div class="post-code"><span class="post-code__language">Perl</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-keyword">print</span> <span class="hljs-string">"Hello, World!\n"</span>;</code></div></div><h3 class="markdown__heading">Bash</h3><div class="post-code"><span class="post-code__language">Bash</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-built_in">echo</span> <span class="hljs-string">"Hello World"</span></code></div></div><h3 class="markdown__heading">PHP</h3><div class="post-code"><span class="post-code__language">PHP</span><div class="hljs"><div class="post-code__line-numbers">1</div><code> <span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">echo</span> <span class="hljs-string">'&lt;p&gt;Hello World&lt;/p&gt;'</span>; <span class="hljs-meta">?&gt;</span> </code></div></div><h3 class="markdown__heading">CoffeeScript</h3><div class="post-code"><span class="post-code__language">CoffeeScript</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-built_in">console</span>.log(“Hello world!”);</code></div></div><h3 class="markdown__heading">C</h3><div class="post-code"><span class="post-code__language">C#</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7
8</div><code><span class="hljs-keyword">using</span> System;
<span class="hljs-keyword">class</span> <span class="hljs-title">Program</span>
{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Main</span>(<span class="hljs-params"><span class="hljs-built_in">string</span>[] args</span>)</span>
{
Console.WriteLine(<span class="hljs-string">"Hello, world!"</span>);
}
}</code></div></div><h3 class="markdown__heading">C++</h3><div class="post-code"><span class="post-code__language">C/C++</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7</div><code><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream.h&gt;</span></span>
main()
{
<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Hello World!"</span>;
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}</code></div></div><h3 class="markdown__heading">SQL</h3><div class="post-code"><span class="post-code__language">SQL</span><div class="hljs"><div class="post-code__line-numbers">1
2</div><code><span class="hljs-keyword">SELECT</span> column_name,column_name
<span class="hljs-keyword">FROM</span> table_name;</code></div></div><h3 class="markdown__heading">Go</h3><div class="post-code"><span class="post-code__language">Go</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5</div><code><span class="hljs-keyword">package</span> main
<span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
fmt.Println(<span class="hljs-string">"Hello, 世界"</span>)
}</code></div></div><h3 class="markdown__heading">Ruby</h3><div class="post-code"><span class="post-code__language">Ruby</span><div class="hljs"><div class="post-code__line-numbers">1</div><code>puts <span class="hljs-string">"Hello, world!"</span></code></div></div><h3 class="markdown__heading">Java</h3><div class="post-code"><span class="post-code__language">Java</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7
8
9
10
11
12</div><code><span class="hljs-keyword">import</span> javax.swing.JFrame; <span class="hljs-comment">//Importing class JFrame</span>
<span class="hljs-keyword">import</span> javax.swing.JLabel; <span class="hljs-comment">//Importing class JLabel</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloWorld</span> </span>{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>{
JFrame frame = <span class="hljs-keyword">new</span> JFrame(); <span class="hljs-comment">//Creating frame</span>
frame.setTitle(<span class="hljs-string">"Hi!"</span>); <span class="hljs-comment">//Setting title frame</span>
frame.add(<span class="hljs-keyword">new</span> JLabel(<span class="hljs-string">"Hello, world!"</span>));<span class="hljs-comment">//Adding text to frame</span>
frame.pack(); <span class="hljs-comment">//Setting size to smallest</span>
frame.setLocationRelativeTo(<span class="hljs-keyword">null</span>); <span class="hljs-comment">//Centering frame</span>
frame.setVisible(<span class="hljs-keyword">true</span>); <span class="hljs-comment">//Showing frame</span>
}
}</code></div></div><h3 class="markdown__heading">Latex Equation</h3><div class="post-body--code tex"><span class="katex-display fleqn"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mi>d</mi><mrow><mi>d</mi><mi>x</mi></mrow></mfrac><mrow><mo fence="true">(</mo><msubsup><mo>∫</mo><mn>0</mn><mi>x</mi></msubsup><mi>f</mi><mo stretchy="false">(</mo><mi>u</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>d</mi><mi>u</mi><mo fence="true">)</mo></mrow><mo>=</mo><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">\frac{d}{dx}\left( \int_{0}^{x} f(u)\,du\right)=f(x).</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:2.40003em;vertical-align:-0.95003em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.37144em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">d</span><span class="mord mathnormal">x</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;"><span class="delimsizing size3">(</span></span><span class="mop"><span class="mop op-symbol large-op" style="margin-right:0.44445em;position:relative;top:-0.0011249999999999316em;">∫</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.414292em;"><span style="top:-1.7880500000000001em;margin-left:-0.44445em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">0</span></span></span></span><span style="top:-3.8129000000000004em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">x</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.9119499999999999em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal">u</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal">d</span><span class="mord mathnormal">u</span><span class="mclose delimcenter" style="top:0em;"><span class="delimsizing size3">)</span></span></span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord">.</span></span></span></span></span></div>

View File

@@ -0,0 +1,7 @@
<h3 class="markdown__heading">Escaped Characters</h3><p><strong>The following text should render the same as the raw text:</strong>
Raw: <span class="codespan__pre-wrap"><code>\\teamlinux\IT-Stuff\WorkingStuff</code></span>
Markdown: \\teamlinux\IT-Stuff\WorkingStuff</p>
<p><strong>The following text should escape out the first backslash so only one backslash appears:</strong>
Raw: <span class="codespan__pre-wrap"><code>\\()#</code></span>
Markdown: \()#</p>
<p>The end of this long post will be hidden until you choose to <span class="codespan__pre-wrap"><code>Show More</code></span>.</p>

View File

@@ -0,0 +1,11 @@
### Escaped Characters
**The following text should render the same as the raw text:**
Raw: `\\teamlinux\IT-Stuff\WorkingStuff`
Markdown: \\teamlinux\IT-Stuff\WorkingStuff
**The following text should escape out the first backslash so only one backslash appears:**
Raw: `\\()#`
Markdown: \\()#
The end of this long post will be hidden until you choose to `Show More`.

View File

@@ -0,0 +1 @@
<h3 class="markdown__heading">Headings</h3><h1 class="markdown__heading">Heading 1 font size</h1><h2 class="markdown__heading">Heading 2 font size</h2><h3 class="markdown__heading">Heading 3 font size</h3><h4 class="markdown__heading">Heading 4 font size</h4><h5 class="markdown__heading">Heading 5 font size</h5><h6 class="markdown__heading">Heading 6 font size</h6>

View File

@@ -0,0 +1,8 @@
### Headings
# Heading 1 font size
## Heading 2 font size
### Heading 3 font size
#### Heading 4 font size
##### Heading 5 font size
###### Heading 6 font size

View File

@@ -0,0 +1,6 @@
<h3 class="markdown__heading">In-line Code</h3><p>The word <span class="codespan__pre-wrap"><code>monospace</code></span> should render as in-line code.</p>
<p>The following markdown in-line code should not render:
<span class="codespan__pre-wrap"><code>_Italics_</code></span>, <span class="codespan__pre-wrap"><code>*Italics*</code></span>, <span class="codespan__pre-wrap"><code>**Bold**</code></span>, <span class="codespan__pre-wrap"><code>***Bold-italics***</code></span>, <span class="codespan__pre-wrap"><code>**Bold-italics_**</code></span>, <span class="codespan__pre-wrap"><code>~~Strikethrough~~</code></span>, <span class="codespan__pre-wrap"><code>:)</code></span> , <span class="codespan__pre-wrap"><code>:-)</code></span> , <span class="codespan__pre-wrap"><code>;)</code></span> , <span class="codespan__pre-wrap"><code>:-O</code></span> , <span class="codespan__pre-wrap"><code>:bamboo:</code></span> , <span class="codespan__pre-wrap"><code>:gift_heart:</code></span> , <span class="codespan__pre-wrap"><code>:dolls:</code></span> , <span class="codespan__pre-wrap"><code># Heading 1</code></span>, <span class="codespan__pre-wrap"><code>## Heading 2</code></span>, <span class="codespan__pre-wrap"><code>### Heading 3</code></span>, <span class="codespan__pre-wrap"><code>#### Heading 4</code></span>, <span class="codespan__pre-wrap"><code>##### Heading 5</code></span>, <span class="codespan__pre-wrap"><code>###### Heading 6</code></span></p>
<p>This GIF link should not preview: <span class="codespan__pre-wrap"><code>http://i.giphy.com/xNrM4cGJ8u3ao.gif</code></span>
This link should not auto-link: <span class="codespan__pre-wrap"><code>https://en.wikipedia.org/wiki/Dolphin</code></span></p>
<p>This sentence with <span class="codespan__pre-wrap"><code>in-line code</code></span> should appear on one line.</p>

View File

@@ -0,0 +1,13 @@
### In-line Code
The word `monospace` should render as in-line code.
The following markdown in-line code should not render:
`_Italics_`, `*Italics*`, `**Bold**`, `***Bold-italics***`, `**Bold-italics_**`, `~~Strikethrough~~`, `:)` , `:-)` , `;)` , `:-O` , `:bamboo:` , `:gift_heart:` , `:dolls:` , `# Heading 1`, `## Heading 2`, `### Heading 3`, `#### Heading 4`, `##### Heading 5`, `###### Heading 6`
This GIF link should not preview: `http://i.giphy.com/xNrM4cGJ8u3ao.gif`
This link should not auto-link: `https://en.wikipedia.org/wiki/Dolphin`
This sentence with `
in-line code
` should appear on one line.

View File

@@ -0,0 +1,3 @@
### In-line Images
Mattermost/platform build status: [![Build Status](https://raw.githubusercontent.com/mattermost/mattermost/master/e2e-tests/cypress/tests/fixtures/mattermost-icon_128x128.png)](https://raw.githubusercontent.com/mattermost/mattermost/master/e2e-tests/cypress/tests/fixtures/mattermost-icon_128x128.png)

View File

@@ -0,0 +1,3 @@
### In-line Images
GitHub favicon: ![Github](https://github.githubassets.com/favicon.ico)

View File

@@ -0,0 +1,4 @@
### In-line Images
GIF Image:
![gif](http://i.giphy.com/xNrM4cGJ8u3ao.gif)

View File

@@ -0,0 +1,4 @@
### In-line Images
4K Wallpaper Image (11Mb):
![4K Image](https://images.wallpaperscraft.com/image/starry_sky_shine_glitter_118976_3840x2160.jpg)

View File

@@ -0,0 +1,4 @@
### In-line Images
Panorama Image:
![Pano](http://amardeepphotography.com/wp-content/uploads/2012/11/Untitled_Panorama6small.jpg)

View File

@@ -0,0 +1,3 @@
### In-line Images
![test image](https://raw.githubusercontent.com/mattermost/mattermost/master/e2e-tests/cypress/tests/fixtures/image-small-height.png)

View File

@@ -0,0 +1,26 @@
<div class="post-code"><span class="post-code__language">LaTeX</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7</div><code><span class="hljs-keyword">\documentclass</span>{article}
<span class="hljs-keyword">\begin</span>{document}
Hello World!
<span class="hljs-keyword">\end</span>{document}</code></div></div><p>AND/OR</p>
<div class="post-code"><span class="post-code__language">LaTeX</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7</div><code><span class="hljs-keyword">\documentclass</span>{article}
<span class="hljs-keyword">\begin</span>{document}
Hello World!
<span class="hljs-keyword">\end</span>{document}</code></div></div>

View File

@@ -0,0 +1,21 @@
```texcode
\documentclass{article}
\begin{document}
Hello World!
\end{document}
```
AND/OR
```latexcode
\documentclass{article}
\begin{document}
Hello World!
\end{document}
```

View File

@@ -0,0 +1,8 @@
<h3 class="markdown__heading">Lines</h3><p>Three lines should render with text between them:</p>
<p>Text above line</p>
<hr>
<p>Text between lines</p>
<hr>
<p>Text between lines</p>
<hr>
<p>Text below line</p>

View File

@@ -0,0 +1,16 @@
### Lines
Three lines should render with text between them:
Text above line
***
Text between lines
---
Text between lines
___
Text below line

View File

@@ -0,0 +1,137 @@
<h1 class="markdown__heading">Markdown List Testing</h1><p>Verify that all list types render as expected.</p>
<h3 class="markdown__heading">Single-Item Ordered List</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>7. Single Item</code></div></div><p><strong>Actual:</strong></p>
<ol class="markdown__list" style="counter-reset: list 6;">
<li><span>Single Item</span></li></ol><h3 class="markdown__heading">Multi-Item Ordered List</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>1. One
2. Two
3. Three</code></div></div><p><strong>Actual:</strong></p>
<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>One</span></li><li><span>Two</span></li><li><span>Three</span></li></ol><h3 class="markdown__heading">Nested Ordered List</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>1. Alpha
1. Bravo
2. Charlie
3. Delta
1. Echo
2. Foxtrot</code></div></div><p><strong>Actual:</strong></p>
<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>Alpha<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>Bravo</span></li></ol></span></li><li><span>Charlie</span></li><li><span>Delta<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>Echo</span></li><li><span>Foxtrot</span></li></ol></span></li></ol><h3 class="markdown__heading">Single-Item Unordered List</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>• Single Item</code></div></div><p><strong>Actual:</strong></p>
<ul class="markdown__list">
<li><span>Single Item</span></li></ul><h3 class="markdown__heading">Multi-Item Unordered List</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>• One
• Two
• Three</code></div></div><p><strong>Actual:</strong></p>
<ul class="markdown__list">
<li><span>One</span></li><li><span>Two</span></li><li><span>Three</span></li></ul><h3 class="markdown__heading">Multi-Item Unordered List with Line Break (Break should not render)</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>• Item A
• Item B
• Item C
• Item D</code></div></div><p><strong>Actual:</strong></p>
<ul class="markdown__list">
<li><span>Item A</span></li><li><span><p>Item B</p>
</span></li><li><span><p>Item C</p>
</span></li><li><span>Item D</span></li></ul><h3 class="markdown__heading">Nested Unordered List</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>• Alpha
• Bravo
• Charlie
• Delta
• Echo
• Foxtrot</code></div></div><p><strong>Actual:</strong></p>
<ul class="markdown__list">
<li><span>Alpha<ul class="markdown__list">
<li><span>Bravo</span></li></ul></span></li><li><span>Charlie</span></li><li><span>Delta<ul class="markdown__list">
<li><span>Echo</span></li><li><span>Foxtrot</span></li></ul></span></li></ul><h3 class="markdown__heading">Mixed List Starting Ordered</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>1. One
2. Two
3. Three</code></div></div><p><strong>Actual:</strong></p>
<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>One</span></li><li><span>Two</span></li><li><span>Three</span></li></ol><h3 class="markdown__heading">Mixed List Starting Unordered</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>• Monday
• Tuesday
• Wednesday</code></div></div><p><strong>Actual:</strong></p>
<ul class="markdown__list">
<li><span>Monday</span></li><li><span>Tuesday</span></li><li><span>Wednesday</span></li></ul><h3 class="markdown__heading">Nested Mixed List</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>• Alpha
1. Bravo
• Charlie
• Delta
• Echo
• Foxtrot
• Golf
1. Hotel
• India
1. Juliet
2. Kilo
• Lima
• Mike
1. November
4. Oscar
5. Papa</code></div></div><p><strong>Actual:</strong></p>
<ul class="markdown__list">
<li><span>Alpha<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>Bravo<ul class="markdown__list">
<li><span>Charlie</span></li><li><span>Delta</span></li></ul></span></li></ol></span></li><li><span>Echo</span></li><li><span>Foxtrot<ul class="markdown__list">
<li><span>Golf<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>Hotel</span></li></ol></span></li><li><span>India<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>Juliet</span></li><li><span>Kilo</span></li></ol></span></li><li><span>Lima</span></li></ul></span></li><li><span>Mike<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>November<ol class="markdown__list" style="counter-reset: list 3;">
<li><span>Oscar<ol class="markdown__list" style="counter-reset: list 4;">
<li><span>Papa</span></li></ol></span></li></ol></span></li></ol></span></li></ul><h3 class="markdown__heading">Ordered Lists Separated by Carriage Returns</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>1. One
• Two
2. Two
3. Three</code></div></div><p><strong>Actual:</strong></p>
<ol class="markdown__list" style="counter-reset: list 0;">
<li><span><p>One</p>
<ul class="markdown__list">
<li><span>Two</span></li></ul></span></li><li><span><p>Two</p>
</span></li><li><span>Three</span></li></ol><h3 class="markdown__heading">New Line After a List</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>1. One
2. Two
This text should be on a new line.</code></div></div><p><strong>Actual:</strong></p>
<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>One</span></li><li><span>Two</span></li></ol><p>This text should be on a new line.</p>
<h3 class="markdown__heading">Task Lists</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>[ ] One
[ ] Subpoint one
- Normal Bullet
[ ] Two
[x] Completed item</code></div></div><p><strong>Actual:</strong></p>
<ul class="markdown__list">
<li class="list-item--task-list"><input type="checkbox" disabled=""> One<ul class="markdown__list">
<li class="list-item--task-list"><input type="checkbox" disabled=""> Subpoint one</li><li><span>Normal Bullet</span></li></ul></li><li class="list-item--task-list"><input type="checkbox" disabled=""> Two</li><li class="list-item--task-list"><input type="checkbox" disabled="" checked=""> Completed item</li></ul><h3 class="markdown__heading">Numbered Task Lists</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>1. [ ] One
2. [ ] Two
3. [x] Completed item</code></div></div><p><strong>Actual:</strong></p>
<ol class="markdown__list" style="counter-reset: list 0;">
<li class="list-item--task-list"><input type="checkbox" disabled=""> One</li><li class="list-item--task-list"><input type="checkbox" disabled=""> Two</li><li class="list-item--task-list"><input type="checkbox" disabled="" checked=""> Completed item</li></ol><h3 class="markdown__heading">Multiple Lists</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>List A:
1. One
List B:
2. Two</code></div></div><p>List A:</p>
<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>One</span></li></ol><p>List B:</p>
<ol class="markdown__list" style="counter-reset: list 1;">
<li><span>Two</span></li></ol><h3 class="markdown__heading">Lists with blank lines before and after</h3><p><strong>Expected:</strong></p>
<div class="post-code post-code--wrap"><div class="hljs"><code>Line with blank line after
Line with blank line after and before
1. Bullet
2. Bullet
3. Bullet
Line with blank line after and before
Line with blank line before</code></div></div><p>Line with blank line after </p>
<p>Line with blank line after and before </p>
<ol class="markdown__list" style="counter-reset: list 0;">
<li><span>Bullet </span></li><li><span>Bullet </span></li><li><span>Bullet </span></li></ol><p>Line with blank line after and before </p>
<p>Line with blank line before</p>

View File

@@ -0,0 +1,3 @@
<p><strong>The following links should not auto-link or generate previews:</strong></p>
<div class="post-code post-code--wrap"><div class="post-code__overlay"><span class="post-code__clipboard"><i role="button" class="icon icon-content-copy"></i></span></div><div class="hljs"><code>GIF: http://i.giphy.com/xNrM4cGJ8u3ao.gif
Website: https://en.wikipedia.org/wiki/Dolphin</code></div></div>

View File

@@ -0,0 +1,5 @@
**The following links should not auto-link or generate previews:**
```
GIF: http://i.giphy.com/xNrM4cGJ8u3ao.gif
Website: https://en.wikipedia.org/wiki/Dolphin
```

View File

@@ -0,0 +1,23 @@
<p><strong>The following markdown should not render:</strong></p>
<div class="post-code post-code--wrap"><div class="post-code__overlay"><span class="post-code__clipboard"><i role="button" class="icon icon-content-copy"></i></span></div><div class="hljs"><code>_Italics_
*Italics*
**Bold**
***Bold-italics***
**Bold-italics_**
~~Strikethrough~~
:) :-) ;) ;-) :o :O :-o :-O
:bamboo: :gift_heart: :dolls: :school_satchel: :mortar_board:
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
&gt; Block Quote
- List
- List Sub-item
[Link](http://i.giphy.com/xNrM4cGJ8u3ao.gif)
[![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
| Left-Aligned Text | Center Aligned Text | Right Aligned Text |
| :------------ |:---------------:| -----:|
| Left column 1 | this text | $100 |</code></div></div>

View File

@@ -0,0 +1,25 @@
**The following markdown should not render:**
```
_Italics_
*Italics*
**Bold**
***Bold-italics***
**Bold-italics_**
~~Strikethrough~~
:) :-) ;) ;-) :o :O :-o :-O
:bamboo: :gift_heart: :dolls: :school_satchel: :mortar_board:
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
> Block Quote
- List
- List Sub-item
[Link](http://i.giphy.com/xNrM4cGJ8u3ao.gif)
[![Github](https://assets-cdn.github.com/favicon.ico)](https://github.com/mattermost/platform)
| Left-Aligned Text | Center Aligned Text | Right Aligned Text |
| :------------ |:---------------:| -----:|
| Left column 1 | this text | $100 |
```

View File

@@ -0,0 +1,50 @@
<div class="post-code"><span class="post-code__language">PostgreSQL</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7</div><code><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR REPLACE</span> <span class="hljs-keyword">FUNCTION</span> snitch() <span class="hljs-keyword">RETURNS</span> <span class="hljs-type">event_trigger</span> <span class="hljs-keyword">AS</span> $$<span class="pgsql">
<span class="hljs-keyword">BEGIN</span>
<span class="hljs-keyword">RAISE</span> <span class="hljs-keyword">NOTICE</span> <span class="hljs-string">'snitch: % %'</span>, <span class="hljs-built_in">tg_event</span>, <span class="hljs-built_in">tg_tag</span>;
<span class="hljs-keyword">END</span>;
$$</span> <span class="hljs-keyword">LANGUAGE</span> plpgsql;
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">EVENT TRIGGER</span> snitch <span class="hljs-keyword">ON</span> ddl_command_start <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">PROCEDURE</span> snitch();</code></div></div><p>and</p>
<div class="post-code"><span class="post-code__language">PostgreSQL</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7</div><code><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR REPLACE</span> <span class="hljs-keyword">FUNCTION</span> snitch() <span class="hljs-keyword">RETURNS</span> <span class="hljs-type">event_trigger</span> <span class="hljs-keyword">AS</span> $$<span class="pgsql">
<span class="hljs-keyword">BEGIN</span>
<span class="hljs-keyword">RAISE</span> <span class="hljs-keyword">NOTICE</span> <span class="hljs-string">'snitch: % %'</span>, <span class="hljs-built_in">tg_event</span>, <span class="hljs-built_in">tg_tag</span>;
<span class="hljs-keyword">END</span>;
$$</span> <span class="hljs-keyword">LANGUAGE</span> plpgsql;
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">EVENT TRIGGER</span> snitch <span class="hljs-keyword">ON</span> ddl_command_start <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">PROCEDURE</span> snitch();</code></div></div><p>and</p>
<div class="post-code"><span class="post-code__language">PostgreSQL</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6
7</div><code><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR REPLACE</span> <span class="hljs-keyword">FUNCTION</span> snitch() <span class="hljs-keyword">RETURNS</span> <span class="hljs-type">event_trigger</span> <span class="hljs-keyword">AS</span> $$<span class="pgsql">
<span class="hljs-keyword">BEGIN</span>
<span class="hljs-keyword">RAISE</span> <span class="hljs-keyword">NOTICE</span> <span class="hljs-string">'snitch: % %'</span>, <span class="hljs-built_in">tg_event</span>, <span class="hljs-built_in">tg_tag</span>;
<span class="hljs-keyword">END</span>;
$$</span> <span class="hljs-keyword">LANGUAGE</span> plpgsql;
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">EVENT TRIGGER</span> snitch <span class="hljs-keyword">ON</span> ddl_command_start <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">PROCEDURE</span> snitch();</code></div></div><p>or</p>
<div class="post-code"><span class="post-code__language">PostgreSQL</span><div class="hljs"><div class="post-code__line-numbers">1
2
3
4
5
6</div><code>CREATE OR REPLACE FUNCTION add(x int, y int)
RETURNS int
LANGUAGE SQL
AS $myfunc$
SELECT x + y
$myfunc$</code></div></div>

View File

@@ -0,0 +1,41 @@
```postgres
CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGIN
RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE PROCEDURE snitch();
```
and
```pgsql
CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGIN
RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE PROCEDURE snitch();
```
and
```postgresql
CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGIN
RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE PROCEDURE snitch();
```
or
```pgsql
CREATE OR REPLACE FUNCTION add(x int, y int)
RETURNS int
LANGUAGE SQL
AS $myfunc$
SELECT x + y
$myfunc$
```

Some files were not shown because too many files have changed in this diff Show More