From 856e38746cbefd6732f0a14183affb5f0b23f401 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 23 Nov 2024 10:40:28 +0000 Subject: [PATCH 001/184] fix: pondersource spelling --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 4cae00e1..1d28158f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 pondersource +Copyright (c) 2023 Ponder Source Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 2ae1ee462f7dff0e56f85ae354d5250d6928ce34 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 26 Nov 2024 07:28:03 +0000 Subject: [PATCH 002/184] add: functions --- scripts/clean.sh | 161 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 128 insertions(+), 33 deletions(-) diff --git a/scripts/clean.sh b/scripts/clean.sh index fa8b4f15..bac08370 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,46 +1,141 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts +# ----------------------------------------------------------------------------------- +# Script to Clean and Re-Initialize Docker Environment for Testing +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Note: This script performs actions that can significantly alter your Docker +# environment by removing all containers, networks, and unused data. Ensure that you +# have backups or that it's safe to perform these operations in your environment +# before running the script. +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status set -e -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "$source" ]; do + dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)" + source="$(readlink "$source")" + [[ "$source" != /* ]] && source="$dir/$source" # Resolve relative symlink + done + dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)" + printf "%s" "$dir" +} + +# ----------------------------------------------------------------------------------- +# Function: modify_cypress_config +# Purpose: Modifies the Cypress configuration file to set 'modifyObstructiveCode' to true. +# ----------------------------------------------------------------------------------- +modify_cypress_config() { + local config_file="$ENV_ROOT/cypress/ocm-test-suite/cypress.config.js" + if [[ -f "$config_file" ]]; then + sed -i 's/.*modifyObstructiveCode: false,.*/ modifyObstructiveCode: true,/' "$config_file" + else + printf "Warning: Configuration file not found: %s\n" "$config_file" >&2 + exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: stop_and_remove_docker_containers +# Purpose: Stops and removes all Docker containers. +# ----------------------------------------------------------------------------------- +stop_and_remove_docker_containers() { + local all_containers + all_containers=$(docker ps -aq) + if [[ -n "$all_containers" ]]; then + # Forcefully stop and remove all containers + docker rm -f $all_containers >/dev/null 2>&1 || true + fi +} + +# ----------------------------------------------------------------------------------- +# Function: docker_cleanup +# Purpose: Cleans up unused Docker volumes and system resources. +# ----------------------------------------------------------------------------------- +docker_cleanup() { + # Prune unused Docker volumes without confirmation + docker volume prune -f >/dev/null 2>&1 || true + # Prune unused Docker system data without confirmation + docker system prune -f >/dev/null 2>&1 || true +} + +# ----------------------------------------------------------------------------------- +# Function: recreate_docker_network +# Purpose: Removes and recreates a specified Docker network. +# Arguments: +# $1 - Name of the Docker network to recreate +# ----------------------------------------------------------------------------------- +recreate_docker_network() { + local network_name="$1" + if [[ -z "$network_name" ]]; then + printf "Error: Network name is required.\n" >&2 + exit 1 + fi + + # Remove the Docker network if it exists + if docker network inspect "$network_name" >/dev/null 2>&1; then + docker network rm "$network_name" >/dev/null 2>&1 || { + printf "Warning: Failed to remove Docker network: %s\n" "$network_name" >&2 + } + fi + + # Create the Docker network + docker network create "$network_name" >/dev/null 2>&1 || { + printf "Error: Failed to create Docker network: %s\n" "$network_name" >&2 + exit 1 + } +} -cd "${DIR}/.." || exit +# ----------------------------------------------------------------------------------- +# Function: main +# Purpose: Main function encapsulating the script logic. +# ----------------------------------------------------------------------------------- +main() { + # Resolve the script's directory and move to the parent directory + local script_dir + script_dir="$(resolve_script_dir)" + cd "$script_dir/.." || { + printf "Error: Failed to change directory to script's parent.\n" >&2 + exit 1 + } -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} + # Export the environment root directory + local env_root + env_root="$(pwd)" + export ENV_ROOT="$env_root" -# revert back to normal. -sed -i 's/.*modifyObstructiveCode: false,.*/ modifyObstructiveCode: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # Modify the Cypress configuration + modify_cypress_config -# clear terminal: yes, no. default is yes. -CLEAR_TERMINAL=${1:-"yes"} + # Determine whether to clear the terminal (default: yes) + local clear_terminal="${1:-yes}" -running=$(docker ps -q) -# we actually need globbing and word spliting in this case. -# shellcheck disable=SC2086 -[ -z "$running" ] || docker kill $running >/dev/null 2>&1 + # Stop and remove all Docker containers + stop_and_remove_docker_containers -existing=$(docker ps -qa) -# we actually need globbing and word spliting in this case. -# shellcheck disable=SC2086 -[ -z "$existing" ] || docker rm $existing >/dev/null 2>&1 + # Clean up unused Docker volumes and system resources + docker_cleanup -echo "y" | docker volume prune -echo "y" | docker system prune + # Recreate the Docker network 'testnet' + recreate_docker_network "testnet" -docker network remove testnet >/dev/null 2>&1 || true >/dev/null 2>&1 -docker network create testnet >/dev/null 2>&1 + # Clear the terminal if requested + if [[ "$clear_terminal" == "yes" ]]; then + clear + fi +} -# I want a clean terminal xD -if [ "${CLEAR_TERMINAL}" = "yes" ]; then - clear -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with all script arguments +# ----------------------------------------------------------------------------------- +main "$@" From e558f1cd20de24a2c51818aafc23189d6cc7c002 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 26 Nov 2024 07:41:42 +0000 Subject: [PATCH 003/184] fix: docker stop and remove func --- scripts/clean.sh | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/clean.sh b/scripts/clean.sh index bac08370..e4d25695 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -12,8 +12,9 @@ # before running the script. # ----------------------------------------------------------------------------------- -# Exit immediately if a command exits with a non-zero status -set -e +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail # ----------------------------------------------------------------------------------- # Function: resolve_script_dir @@ -50,12 +51,7 @@ modify_cypress_config() { # Purpose: Stops and removes all Docker containers. # ----------------------------------------------------------------------------------- stop_and_remove_docker_containers() { - local all_containers - all_containers=$(docker ps -aq) - if [[ -n "$all_containers" ]]; then - # Forcefully stop and remove all containers - docker rm -f $all_containers >/dev/null 2>&1 || true - fi + docker ps -q | xargs -r docker stop && docker ps -q -a | xargs -r docker rm } # ----------------------------------------------------------------------------------- From 014fc334dbda8189a285cdc363209effb76c4f7b Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 26 Nov 2024 07:42:02 +0000 Subject: [PATCH 004/184] add: functions and eeror handling --- scripts/switch-php.sh | 195 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 183 insertions(+), 12 deletions(-) diff --git a/scripts/switch-php.sh b/scripts/switch-php.sh index 4880caf3..0d9c0f86 100755 --- a/scripts/switch-php.sh +++ b/scripts/switch-php.sh @@ -1,18 +1,189 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts -set -e +# ----------------------------------------------------------------------------------- +# Script to Configure PHP Version Alternatives on the System +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- -FILE="/usr/bin/php${1}" +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail -if [[ -n "${1}" ]]; then - if [[ -f "${FILE}" ]]; then - update-alternatives --set php "/usr/bin/php${1}" - update-alternatives --set phar "/usr/bin/phar${1}" - update-alternatives --set phar.phar "/usr/bin/phar.phar${1}" +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="$1" + printf "Error: %s\n" "$message" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: usage +# Purpose: Display usage instructions for the script. +# ----------------------------------------------------------------------------------- +usage() { + printf "Usage: %s \n" "$(basename "$0")" >&2 + printf "Example: %s 8.1\n" "$(basename "$0")" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + local command="$1" + command -v "$command" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Function: check_php_binaries_exist +# Purpose: Check if the specified PHP version binaries exist. +# Arguments: +# $1 - The PHP version to check. +# Returns: +# 0 if all binaries exist, exits the script otherwise. +# ----------------------------------------------------------------------------------- +check_php_binaries_exist() { + local version="$1" + local php_bin="/usr/bin/php$version" + local phar_bin="/usr/bin/phar$version" + local phar_phar_bin="/usr/bin/phar.phar$version" + local missing_binaries=() + + # Check for php binary + if [[ ! -x "$php_bin" ]]; then + missing_binaries+=("$php_bin") + fi + + # Check for phar binary + if [[ ! -x "$phar_bin" ]]; then + missing_binaries+=("$phar_bin") + fi + + # Check for phar.phar binary + if [[ ! -x "$phar_phar_bin" ]]; then + missing_binaries+=("$phar_phar_bin") + fi + + if [[ ${#missing_binaries[@]} -gt 0 ]]; then + print_error "The following PHP binaries for version $version are missing or not executable:" + for bin in "${missing_binaries[@]}"; do + printf " %s\n" "$bin" >&2 + done + printf "Please ensure PHP version %s is installed correctly.\n" "$version" >&2 + exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: update_php_alternatives +# Purpose: Update the PHP alternatives to point to the specified version. +# Arguments: +# $1 - The PHP version to set. +# Returns: +# 0 if successful, exits the script otherwise. +# ----------------------------------------------------------------------------------- +update_php_alternatives() { + local version="$1" + local php_bin="/usr/bin/php$version" + local phar_bin="/usr/bin/phar$version" + local phar_phar_bin="/usr/bin/phar.phar$version" + + printf "Configuring PHP alternatives to version %s...\n" "$version" + + # Update 'php' alternative + if ! sudo update-alternatives --set php "$php_bin"; then + print_error "Failed to set 'php' alternative to $php_bin." + exit 1 + fi + + # Update 'phar' alternative + if ! sudo update-alternatives --set phar "$phar_bin"; then + print_error "Failed to set 'phar' alternative to $phar_bin." + exit 1 + fi + + # Update 'phar.phar' alternative + if ! sudo update-alternatives --set phar.phar "$phar_phar_bin"; then + print_error "Failed to set 'phar.phar' alternative to $phar_phar_bin." + exit 1 + fi + + printf "PHP version successfully set to %s.\n" "$version" +} + +# ----------------------------------------------------------------------------------- +# Function: check_alternative_exists +# Purpose: Check if an alternative group exists. +# Arguments: +# $1 - The alternative name to check (e.g., 'php'). +# Returns: +# 0 if the alternative exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +check_alternative_exists() { + local alt_name="$1" + if update-alternatives --query "$alt_name" >/dev/null 2>&1; then + return 0 else - echo "This version is not available in this system." + return 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: main +# Purpose: Main function to configure PHP alternatives. +# Arguments: +# $@ - The command-line arguments passed to the script. +# ----------------------------------------------------------------------------------- +main() { + # Ensure a version number is provided as the first argument + if [[ $# -ne 1 ]]; then + print_error "Missing required PHP version number." + usage + exit 1 + fi + + local version="$1" + + # Check if 'update-alternatives' command is available + if ! command_exists "update-alternatives"; then + print_error "'update-alternatives' command not found. Please install it and try again." + exit 1 + fi + + # Check if the required PHP binaries exist + check_php_binaries_exist "$version" + + # Check if the alternatives exist + local alternatives_missing=() + for alt in php phar phar.phar; do + if ! check_alternative_exists "$alt"; then + alternatives_missing+=("$alt") + fi + done + + if [[ ${#alternatives_missing[@]} -gt 0 ]]; then + print_error "The following alternatives do not exist:" + for alt in "${alternatives_missing[@]}"; do + printf " %s\n" "$alt" >&2 + done + printf "Please set up the alternatives for PHP versions before switching.\n" >&2 + printf "You may need to run 'sudo update-alternatives --install' for each alternative.\n" >&2 + exit 1 fi -else - echo "You didn't provide any version number!" -fi + + # Update PHP alternatives + update_php_alternatives "$version" +} + +# ----------------------------------------------------------------------------------- +# Entry Point: Execute the main function with all script arguments +# ----------------------------------------------------------------------------------- +main "$@" From 3802f574cb9602bbed4ddb00bb59c283f0c7f4f3 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 26 Nov 2024 08:49:19 +0000 Subject: [PATCH 005/184] refactor: reva scripts --- scripts/reva-cli-einstein.sh | 7 - scripts/reva-cli-marie.sh | 7 - scripts/reva-kill-run.sh | 16 -- scripts/reva-restart.sh | 11 -- scripts/reva/cli.sh | 150 +++++++++++++++++++ scripts/{reva-rebuild.sh => reva/rebuild.sh} | 1 + scripts/reva/restart_container.sh | 77 ++++++++++ scripts/reva/restart_in_conatiner.sh | 117 +++++++++++++++ 8 files changed, 345 insertions(+), 41 deletions(-) delete mode 100755 scripts/reva-cli-einstein.sh delete mode 100755 scripts/reva-cli-marie.sh delete mode 100755 scripts/reva-kill-run.sh delete mode 100755 scripts/reva-restart.sh create mode 100755 scripts/reva/cli.sh rename scripts/{reva-rebuild.sh => reva/rebuild.sh} (97%) create mode 100755 scripts/reva/restart_container.sh create mode 100755 scripts/reva/restart_in_conatiner.sh diff --git a/scripts/reva-cli-einstein.sh b/scripts/reva-cli-einstein.sh deleted file mode 100755 index c9f0d1a6..00000000 --- a/scripts/reva-cli-einstein.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts -set -e - -# run /reva/cmd/reva/reva inside /bin/bash -c "..." so /root/.reva-token is accessible -docker exec -it revad1.docker /bin/bash -c "/reva/cmd/reva/reva -insecure -host localhost:19000" diff --git a/scripts/reva-cli-marie.sh b/scripts/reva-cli-marie.sh deleted file mode 100755 index 26e04c26..00000000 --- a/scripts/reva-cli-marie.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts -set -e - -# run /reva/cmd/reva/reva inside /bin/bash -c "..." so /root/.reva-token is accessible -docker exec -it revad2.docker /bin/bash -c "/reva/cmd/reva/reva -insecure -host localhost:19000" diff --git a/scripts/reva-kill-run.sh b/scripts/reva-kill-run.sh deleted file mode 100755 index a83f3f07..00000000 --- a/scripts/reva-kill-run.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts -set -e - -# get reva container names. (this assumes only 2 containers with reva in their names exist) -REVA1=$(docker ps --filter "name=reva" --format "{{.Names}}" | tail -1) -REVA2=$(docker ps --filter "name=reva" --format "{{.Names}}" | head -1) - -# kill reva. -docker exec "${REVA1}" bash -c "reva-kill.sh" -docker exec "${REVA2}" bash -c "reva-kill.sh" - -# run revad. -docker exec "${REVA1}" bash -c "reva-run.sh" -docker exec "${REVA2}" bash -c "reva-run.sh" diff --git a/scripts/reva-restart.sh b/scripts/reva-restart.sh deleted file mode 100755 index 6d799ef9..00000000 --- a/scripts/reva-restart.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts -set -e - -# get reva container names. (this assumes only 2 containers with reva in their names exist) -REVA1=$(docker ps --filter "name=reva" --format "{{.Names}}" | tail -1) -REVA2=$(docker ps --filter "name=reva" --format "{{.Names}}" | head -1) - -docker restart "${REVA1}" -docker restart "${REVA2}" diff --git a/scripts/reva/cli.sh b/scripts/reva/cli.sh new file mode 100755 index 00000000..a01244a9 --- /dev/null +++ b/scripts/reva/cli.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Execute the Reva Command-Line Tool Inside a Docker Container +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script executes the Reva command-line tool inside a running Docker container. +# It validates the container's state, handles errors gracefully, and ensures modularity. + +# Usage: +# ./cli.sh [container_name] [reva_command] + +# Arguments: +# container_name : (Optional) Name of the Docker container. Defaults to "revad1.docker". +# reva_command : (Optional) Reva command to execute. Defaults to "/reva/cmd/reva/reva -insecure -host localhost:19000". + +# Requirements: +# - Docker must be installed and accessible to the current user. +# - A running Docker container with the specified name. +# - The Reva binary should exist inside the container at the specified path. +# - The script must be executed by a user with permissions to run Docker commands. + +# Examples: +# ./cli.sh +# Executes the default Reva command inside the "revad1.docker" container. +# +# ./cli.sh my_container "/usr/local/bin/reva -help" +# Executes the Reva help command inside the "my_container" container. + +# ----------------------------------------------------------------------------------- + +# Exit immediately on any error, treat unset variables as an error, and catch errors in pipelines. +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Prints an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="$1" + printf "Error: %s\n" "$message" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: validate_docker_installed +# Purpose: Validate that Docker is installed and available in the system PATH. +# ----------------------------------------------------------------------------------- +validate_docker_installed() { + if ! command -v docker >/dev/null 2>&1; then + print_error "Docker is not installed or not available in the system PATH." + exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: validate_container_running +# Purpose: Validate if the Docker container is running. +# Arguments: +# $1 - The name of the container. +# ----------------------------------------------------------------------------------- +validate_container_running() { + local container_name="$1" + if ! docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then + print_error "Docker container '${container_name}' is not running. Please start the container and try again." + exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: check_reva_exists_in_container +# Purpose: Check if the Reva binary exists inside the container. +# Arguments: +# $1 - The name of the container. +# $2 - The path to the Reva binary or command inside the container. +# ----------------------------------------------------------------------------------- +check_reva_exists_in_container() { + local container_name="$1" + local reva_command="$2" + + # Extract the command path (assumes the command is the first word) + local reva_path + reva_path=$(echo "$reva_command" | awk '{print $1}') + + # Check if the file exists inside the container + if ! docker exec "$container_name" test -x "$reva_path"; then + print_error "Reva binary not found or not executable at '$reva_path' inside container '$container_name'." + exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: run_reva_command +# Purpose: Execute the Reva CLI command inside the Docker container. +# Arguments: +# $1 - The name of the container. +# $2 - The Reva command to execute. +# ----------------------------------------------------------------------------------- +run_reva_command() { + local container_name="$1" + local reva_command="$2" + + # Execute the command inside the container. + if ! docker exec -it "$container_name" /bin/bash -c "$reva_command"; then + print_error "Failed to execute the Reva command inside container '$container_name'." + exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: main +# Purpose: Main function to encapsulate script logic. +# ----------------------------------------------------------------------------------- +main() { + # Default values + local container_name="revad1.docker" + # TODO: @MahdiBaghbani probably outdated, checck it soon. + local reva_command="/reva/cmd/reva/reva -insecure -host localhost:19000" + + # Override defaults with arguments if provided + if [ $# -ge 1 ]; then + container_name="$1" + fi + + if [ $# -ge 2 ]; then + reva_command="$2" + fi + + # Validate prerequisites. + validate_docker_installed + validate_container_running "$container_name" + + # Check if Reva exists inside the container + check_reva_exists_in_container "$container_name" "$reva_command" + + # Execute the Reva command. + run_reva_command "$container_name" "$reva_command" + + # Print success message. + printf "Successfully executed the Reva command in container '%s'.\n" "$container_name" +} + + +# ----------------------------------------------------------------------------------- +# Execute the main function +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/scripts/reva-rebuild.sh b/scripts/reva/rebuild.sh similarity index 97% rename from scripts/reva-rebuild.sh rename to scripts/reva/rebuild.sh index 335021d9..30ec96d5 100755 --- a/scripts/reva-rebuild.sh +++ b/scripts/reva/rebuild.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash +# TODO: this is outdated, fix it soon. # @michielbdejong halt on error in docker init scripts set -e diff --git a/scripts/reva/restart_container.sh b/scripts/reva/restart_container.sh new file mode 100755 index 00000000..59904af0 --- /dev/null +++ b/scripts/reva/restart_container.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Restart All Docker Containers with 'reva' in Their Names +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# an undefined variable is used, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Prints an error message to stderr and exits the script. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="$1" + printf "Error: %s\n" "$message" >&2 + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: restart_container +# Purpose: Restarts a Docker container by name. +# Arguments: +# $1 - The name of the container to restart. +# ----------------------------------------------------------------------------------- +restart_container() { + local container_name="$1" + + # Check if container name is provided + if [[ -z "$container_name" ]]; then + printf "Warning: Container name is empty. Skipping.\n" >&2 + return 1 + fi + + # Attempt to restart the container + if docker restart "$container_name" >/dev/null 2>&1; then + printf "Successfully restarted container: %s\n" "$container_name" + else + printf "Failed to restart container: %s\n" "$container_name" >&2 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: main +# Purpose: Main function to restart all Docker containers with 'reva' in their names. +# ----------------------------------------------------------------------------------- +main() { + # Fetch all container names matching "reva" (both running and stopped) + local reva_containers + if ! reva_containers=$(docker ps -a --filter "name=reva" --format "{{.Names}}"); then + print_error "Failed to fetch container names matching 'reva'." + fi + + # Check if any containers were found + if [[ -z "$reva_containers" ]]; then + printf "No containers with 'reva' in their names were found.\n" + exit 0 + fi + + printf "Found the following 'reva' containers:\n%s\n" "$reva_containers" + + # Restart each container + while IFS= read -r container; do + restart_container "$container" + done <<< "$reva_containers" + + printf "All 'reva' containers have been processed.\n" +} + +# ----------------------------------------------------------------------------------- +# Execute the main function +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/scripts/reva/restart_in_conatiner.sh b/scripts/reva/restart_in_conatiner.sh new file mode 100755 index 00000000..2129c7c4 --- /dev/null +++ b/scripts/reva/restart_in_conatiner.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Restart the 'reva' Process in All Docker Containers with 'reva' in Their Names +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# an undefined variable is used, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Prints an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="$1" + printf "Error: %s\n" "$message" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: restart_reva_in_container +# Purpose: Restarts the 'reva' process inside a Docker container by name. +# Arguments: +# $1 - The name of the container. +# ----------------------------------------------------------------------------------- +restart_reva_in_container() { + local container_name="$1" + local success=true + + # Check if container name is provided + if [[ -z "$container_name" ]]; then + printf "Warning: Container name is empty. Skipping.\n" >&2 + return 1 + fi + + # Check if the container is running + if ! docker ps --format '{{.Names}}' | grep -qw "^${container_name}$"; then + printf "Container '%s' is not running. Starting it...\n" "$container_name" + if ! docker start "$container_name" >/dev/null 2>&1; then + print_error "Failed to start container: $container_name" + success=false + else + printf "Container '%s' started successfully.\n" "$container_name" + fi + fi + + printf "Restarting 'reva' process in container: %s\n" "$container_name" + + # Kill 'reva' process inside the container + if ! docker exec "$container_name" bash -c "reva-kill.sh"; then + print_error "Failed to kill 'reva' process in container: $container_name" + success=false + else + printf "Successfully killed 'reva' process in container: %s\n" "$container_name" + fi + + # Start 'reva' process inside the container + if ! docker exec "$container_name" bash -c "reva-run.sh"; then + print_error "Failed to start 'reva' process in container: $container_name" + success=false + else + printf "Successfully started 'reva' process in container: %s\n" "$container_name" + fi + + if [ "$success" = true ]; then + printf "'reva' process successfully restarted in container: %s\n" "$container_name" + return 0 + else + print_error "Errors occurred while processing container: $container_name" + return 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: main +# Purpose: Main function to restart 'reva' process in all Docker containers with 'reva' in their names. +# ----------------------------------------------------------------------------------- +main() { + # Fetch all container names matching "reva" (both running and stopped) + local reva_containers + if ! reva_containers=$(docker ps -a --filter "name=reva" --format "{{.Names}}"); then + print_error "Failed to fetch container names matching 'reva'." + exit 1 + fi + + # Check if any containers were found + if [[ -z "$reva_containers" ]]; then + printf "No containers with 'reva' in their names were found.\n" + exit 0 + fi + + printf "Found the following 'reva' containers:\n%s\n" "$reva_containers" + + # Restart 'reva' process in each container + local overall_success=true + while IFS= read -r container; do + if ! restart_reva_in_container "$container"; then + overall_success=false + fi + done <<< "$reva_containers" + + if [ "$overall_success" = true ]; then + printf "All 'reva' containers have been processed successfully.\n" + exit 0 + else + print_error "Some containers encountered errors during processing." + exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function +# ----------------------------------------------------------------------------------- +main "$@" From 38f745f348bba7fa405d52f639859a3e4c242ec1 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 26 Nov 2024 08:49:33 +0000 Subject: [PATCH 006/184] refactotr: mariadb script --- scripts/db-maria.sh | 186 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 169 insertions(+), 17 deletions(-) diff --git a/scripts/db-maria.sh b/scripts/db-maria.sh index 6e6153b1..414479f4 100755 --- a/scripts/db-maria.sh +++ b/scripts/db-maria.sh @@ -1,21 +1,173 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts -set -e +# ----------------------------------------------------------------------------------- +# Script to Execute MariaDB Commands Inside a Docker Container +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- -# syntax: -# db-maria.sh platform number -# +# Description: +# This script executes a specified MariaDB command inside a Docker container. +# It is intended for platforms such as ownCloud or Nextcloud, using Dockerized MariaDB instances. + +# Usage: +# db-maria.sh [] + +# Arguments: +# platform : Platform name (e.g., 'owncloud' or 'nextcloud'). Must be alphanumeric. +# number : A unique numeric identifier for the MariaDB container instance. +# db_command : (Optional) MariaDB database name or SQL command to execute. +# If not provided, the script will open an interactive MariaDB shell. + +# Requirements: +# - A running Docker container with a name matching "maria.docker". +# - Docker must be installed and configured for the current user. +# - Environment variable `DB_PASSWORD` should contain the root password for MariaDB. +# Alternatively, the script will prompt for the password securely. + +# Examples: +# ./db-maria.sh owncloud 1 my_database +# Connects to database 'my_database' inside the container 'mariaowncloud1.docker'. # -# platform: owncloud, nextcloud. -# number: should be unique for each platform, you cannot have two nextclouds with same number. - -platform=${1} -number=${2} - -docker exec -it \ - "maria${platform}${number}.docker" \ - mariadb:11.4.2 \ - -u root \ - -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - efss +# ./db-maria.sh owncloud 1 -e "SHOW DATABASES;" +# Executes the SQL command 'SHOW DATABASES;' inside the container 'mariaowncloud1.docker'. + +# ----------------------------------------------------------------------------------- + +# Exit immediately on any error, treat unset variables as an error, and catch errors in pipelines. +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr and exit with a failure code. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="$1" + printf "Error: %s\n" "$message" >&2 + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: validate_alphanumeric +# Purpose: Validate that an argument is alphanumeric. +# Arguments: +# $1 - The argument to validate. +# ----------------------------------------------------------------------------------- +validate_alphanumeric() { + local arg="$1" + if [[ ! "$arg" =~ ^[a-zA-Z0-9]+$ ]]; then + print_error "Invalid value '$arg'. Only alphanumeric characters are allowed." + fi +} + +# ----------------------------------------------------------------------------------- +# Function: validate_numeric +# Purpose: Validate that an argument is numeric. +# Arguments: +# $1 - The argument to validate. +# ----------------------------------------------------------------------------------- +validate_numeric() { + local arg="$1" + if [[ ! "$arg" =~ ^[0-9]+$ ]]; then + print_error "Invalid value '$arg'. It must be a numeric value." + fi +} + +# ----------------------------------------------------------------------------------- +# Function: prompt_for_password +# Purpose: Prompt the user for the MariaDB root password securely. +# Sets the global variable 'DB_PASSWORD'. +# ----------------------------------------------------------------------------------- +prompt_for_password() { + read -s -p "Enter MariaDB root password: " DB_PASSWORD + echo + if [[ -z "$DB_PASSWORD" ]]; then + print_error "Password cannot be empty." + fi +} + +# ----------------------------------------------------------------------------------- +# Function: check_container_running +# Purpose: Check if the Docker container is running. +# Arguments: +# $1 - The name of the container. +# ----------------------------------------------------------------------------------- +check_container_running() { + local container_name="$1" + if ! docker ps --format '{{.Names}}' | grep -qw "^${container_name}$"; then + print_error "Docker container '${container_name}' is not running." + fi +} + +# ----------------------------------------------------------------------------------- +# Function: execute_mariadb_command +# Purpose: Execute the MariaDB command inside the Docker container. +# Arguments: +# $1 - The name of the container. +# $2 - The database name or SQL command to execute. +# ----------------------------------------------------------------------------------- +execute_mariadb_command() { + local container_name="$1" + shift + local mariadb_args=("$@") + + # Avoid passing password via command-line arguments + # Use MYSQL_PWD environment variable inside the container + if ! docker exec -i -e MYSQL_PWD="${DB_PASSWORD}" "${container_name}" mariadb -u root "${mariadb_args[@]}"; then + print_error "Failed to execute MariaDB command inside container '${container_name}'." + fi +} + +# ----------------------------------------------------------------------------------- +# Function: main +# Purpose: Main function to Execute MariaDB Commands Inside a Docker Container. +# ----------------------------------------------------------------------------------- +main() { + # Check if at least two arguments are provided. + if [[ $# -lt 2 ]]; then + print_error "Usage: $0 []" + fi + + # Assign and sanitize script arguments. + platform="$1" + number="$2" + shift 2 + db_command=("$@") # Remaining arguments + + validate_alphanumeric "$platform" + validate_numeric "$number" + + # Construct Docker container name. + container_name="maria${platform}${number}.docker" + + # Check if the Docker container is running. + check_container_running "$container_name" + + # @MahdiBaghbani: Yeah I know not the best way to do it, + # but remember this is development environment so ... who cares xD + export DB_PASSWORD="peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + + # Ensure the MariaDB root password is provided via the DB_PASSWORD environment variable or prompt for it. + if [[ -z "${DB_PASSWORD:-}" ]]; then + prompt_for_password + fi + + # If no command is provided, open an interactive MariaDB shell. + if [[ ${#db_command[@]} -eq 0 ]]; then + echo "No command provided. Opening interactive MariaDB shell inside container '${container_name}'." + docker exec -it -e MYSQL_PWD="${DB_PASSWORD}" "${container_name}" mariadb -u root + exit 0 + fi + + # Execute the MariaDB command inside the container. + execute_mariadb_command "$container_name" "${db_command[@]}" + + # Success message. + printf "Command '%s' executed successfully in container '%s'.\n" "${db_command[*]}" "${container_name}" +} + +# ----------------------------------------------------------------------------------- +# Execute the main function +# ----------------------------------------------------------------------------------- +main "$@" From 51f75e097e4844b51f627edb66b014e1ecbf89e9 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 2 Dec 2024 10:18:30 +0000 Subject: [PATCH 007/184] add: ss command --- docker/dockerfiles/revad.Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/dockerfiles/revad.Dockerfile b/docker/dockerfiles/revad.Dockerfile index f8f062c1..c346866c 100644 --- a/docker/dockerfiles/revad.Dockerfile +++ b/docker/dockerfiles/revad.Dockerfile @@ -54,6 +54,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --ass bash \ curl \ tzdata \ + iproute2 \ ca-certificates ENV TZ=Etc/UTC From acb7021731698173f26592606ce1b0e881e23f80 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 2 Dec 2024 10:30:38 +0000 Subject: [PATCH 008/184] fix: style --- scripts/clean.sh | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/scripts/clean.sh b/scripts/clean.sh index e4d25695..17c31cef 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -19,17 +19,20 @@ set -euo pipefail # ----------------------------------------------------------------------------------- # Function: resolve_script_dir # Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. # ----------------------------------------------------------------------------------- resolve_script_dir() { local source="${BASH_SOURCE[0]}" local dir - while [ -L "$source" ]; do - dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)" - source="$(readlink "$source")" - [[ "$source" != /* ]] && source="$dir/$source" # Resolve relative symlink + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" done - dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)" - printf "%s" "$dir" + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" } # ----------------------------------------------------------------------------------- @@ -37,11 +40,11 @@ resolve_script_dir() { # Purpose: Modifies the Cypress configuration file to set 'modifyObstructiveCode' to true. # ----------------------------------------------------------------------------------- modify_cypress_config() { - local config_file="$ENV_ROOT/cypress/ocm-test-suite/cypress.config.js" - if [[ -f "$config_file" ]]; then - sed -i 's/.*modifyObstructiveCode: false,.*/ modifyObstructiveCode: true,/' "$config_file" + local config_file="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + if [[ -f "${config_file}" ]]; then + sed -i 's/.*modifyObstructiveCode: false,.*/ modifyObstructiveCode: true,/' "${config_file}" else - printf "Warning: Configuration file not found: %s\n" "$config_file" >&2 + printf "Warning: Configuration file not found: %s\n" "${config_file}" >&2 exit 1 fi } @@ -51,7 +54,7 @@ modify_cypress_config() { # Purpose: Stops and removes all Docker containers. # ----------------------------------------------------------------------------------- stop_and_remove_docker_containers() { - docker ps -q | xargs -r docker stop && docker ps -q -a | xargs -r docker rm + docker ps -q | xargs -r docker stop && docker ps -q -a | xargs -r docker rm } # ----------------------------------------------------------------------------------- @@ -72,22 +75,22 @@ docker_cleanup() { # $1 - Name of the Docker network to recreate # ----------------------------------------------------------------------------------- recreate_docker_network() { - local network_name="$1" - if [[ -z "$network_name" ]]; then + local network_name="${1}" + if [[ -z "${network_name}" ]]; then printf "Error: Network name is required.\n" >&2 exit 1 fi # Remove the Docker network if it exists - if docker network inspect "$network_name" >/dev/null 2>&1; then - docker network rm "$network_name" >/dev/null 2>&1 || { - printf "Warning: Failed to remove Docker network: %s\n" "$network_name" >&2 + if docker network inspect "${network_name}" >/dev/null 2>&1; then + docker network rm "${network_name}" >/dev/null 2>&1 || { + printf "Warning: Failed to remove Docker network: %s\n" "${network_name}" >&2 } fi # Create the Docker network - docker network create "$network_name" >/dev/null 2>&1 || { - printf "Error: Failed to create Docker network: %s\n" "$network_name" >&2 + docker network create "${network_name}" >/dev/null 2>&1 || { + printf "Error: Failed to create Docker network: %s\n" "${network_name}" >&2 exit 1 } } @@ -100,7 +103,7 @@ main() { # Resolve the script's directory and move to the parent directory local script_dir script_dir="$(resolve_script_dir)" - cd "$script_dir/.." || { + cd "${script_dir}/.." || { printf "Error: Failed to change directory to script's parent.\n" >&2 exit 1 } @@ -108,7 +111,7 @@ main() { # Export the environment root directory local env_root env_root="$(pwd)" - export ENV_ROOT="$env_root" + export ENV_ROOT="${env_root}" # Modify the Cypress configuration modify_cypress_config @@ -117,7 +120,7 @@ main() { local clear_terminal="${1:-yes}" # Stop and remove all Docker containers - stop_and_remove_docker_containers + stop_and_remove_docker_containers >/dev/null 2>&1 # Clean up unused Docker volumes and system resources docker_cleanup @@ -126,7 +129,7 @@ main() { recreate_docker_network "testnet" # Clear the terminal if requested - if [[ "$clear_terminal" == "yes" ]]; then + if [[ "${clear_terminal}" == "yes" ]]; then clear fi } From fc19ff533d929dbd6859e177c232aaa893055b27 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 2 Dec 2024 10:31:27 +0000 Subject: [PATCH 009/184] refactored and tested to work correctly --- .../invite-link/nextcloud-nextcloud.sh | 803 +++++++++++------- 1 file changed, 498 insertions(+), 305 deletions(-) diff --git a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh index 65eb1a36..8dcbab98 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh @@ -1,41 +1,160 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.12 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.12 -EFSS_PLATFORM_2_VERSION=${2:-"v27.1.11"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to Nextcloud OCM invite link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the primary EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the secondary EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-nextcloud.sh v28.0.12 v27.1.11 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_VERSION="v27.1.11" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containerS +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1@sha256:e9bb8aa3e4cca25867c1bdb09bd0a334957fc26ec25239534e6909697efb297e +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1@sha256:ea3ef3febbfadb876955c2eaff5dde4772f70676cd318e0e3706c5ddc0fd9e68 +MARIADB_REPO=mariadb +MARIADB_TAG=11.6.2@sha256:0a620383fe05d20b3cc7510ebccc6749f83f1b0f97f3030d10dd2fa199371f07 +VNC_REPO=theasp/novnc +VNC_TAG=latest@sha256:26dcdccd36e5a6f6eb93beb76c8a74d5a5120a58184433f948428bb018d54c58 + + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { if [ "${SCRIPT_MODE}" = "ci" ]; then "$@" >/dev/null 2>&1 else @@ -43,282 +162,356 @@ function redirect_to_null_cmd() { fi } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" } -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- + +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Initialization script filename. +# $5 - EFSS platform version (optional). +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local init_script="${4}" + local version="${6:-$DEFAULT_EFSS_VERSION}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Validate that the init script exists + if [ ! -f "${ENV_ROOT}/${TEMP_DIR}/${init_script}" ]; then + error_exit "Initialization script not found: ${ENV_ROOT}/${TEMP_DIR}/${init_script}" + fi + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e DBHOST="marianextcloud${number}.docker" \ + -e USER="${user}" \ + -e PASS="${password}" \ + -v "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}:/certificates" \ + -v "${ENV_ROOT}/${TLS_CA_DIR}:/certificate-authority" \ + -v "${ENV_ROOT}/${TEMP_DIR}/${init_script}":"/init.sh" \ + -v "${ENV_ROOT}/docker/scripts/entrypoint.sh":"/entrypoint.sh" \ + "pondersource/dev-stock-nextcloud-sciencemesh:latest" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 + + # Install and update certificates inside the EFSS container + run_quietly_if_ci docker exec "nextcloud${number}.docker" sh -c "cp /certificates/*.crt /usr/local/share/ca-certificates/ || true" + run_quietly_if_ci docker exec "nextcloud${number}.docker" sh -c "update-ca-certificates" || error_exit "Failed to update CA certificates in ${platform} ${number}." + # Run the initialization script inside EFSS + run_quietly_if_ci docker exec -u www-data "nextcloud${number}.docker" sh -c "/init.sh" || error_exit "Initialization script failed for ${platform} ${number}." } -function createReva() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "creating reva for ${platform} ${number}" - - # make sure scripts are executable. - chmod +x "${ENV_ROOT}/temp/reva/run.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/kill.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/entrypoint.sh" >/dev/null 2>&1 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/temp/reva/run.sh:/usr/bin/run.sh" \ - -v "${ENV_ROOT}/temp/reva/kill.sh:/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/temp/reva/entrypoint.sh:/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad +# ----------------------------------------------------------------------------------- +# Function: create_reva +# Purpose: Create a Reva container for the specified EFSS platform and instance. +# Arguments: +# $1 - EFSS platform (e.g., nextcloud). +# $2 - Instance number. +# ----------------------------------------------------------------------------------- +create_reva() { + local platform="${1}" + local number="${2}" + + run_quietly_if_ci echo "Creating Reva instance: ${platform} ${number}" + + # Ensure Reva scripts are executable + run_quietly_if_ci chmod +x "${ENV_ROOT}/${TEMP_DIR}/reva/"{run.sh,kill.sh,entrypoint.sh} || error_exit "Failed to make Reva scripts executable." + + # Start Reva container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="reva${platform}${number}.docker" \ + -e HOST="reva${platform}${number}" \ + -v "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}:/certificates" \ + -v "${ENV_ROOT}/${TLS_CA_DIR}:/certificate-authority" \ + -v "${ENV_ROOT}/${TEMP_DIR}/reva/configs:/configs/revad" \ + -v "${ENV_ROOT}/${TEMP_DIR}/reva/run.sh":"/usr/bin/run.sh" \ + -v "${ENV_ROOT}/${TEMP_DIR}/reva/kill.sh":"/usr/bin/kill.sh" \ + -v "${ENV_ROOT}/${TEMP_DIR}/reva/entrypoint.sh":"/usr/bin/entrypoint.sh" \ + pondersource/dev-stock-revad || error_exit "Failed to start Reva container for ${platform} ${number}." + + # Wait for Reva port to open (assuming Reva uses port 19000) + wait_for_port "reva${platform}${number}.docker" 19000 } -function sciencemeshInsertIntoDB() { - local platform="${1}" - local number="${2}" - redirect_to_null_cmd echo "configuring ScienceMesh app for efss ${platform} ${number}" +# ----------------------------------------------------------------------------------- +# Function: configure_sciencemesh +# Purpose: Configure ScienceMesh settings for the EFSS platform. +# Arguments: +# $1 - EFSS platform (e.g., nextcloud). +# $2 - Instance number. +# ----------------------------------------------------------------------------------- +configure_sciencemesh() { + local platform="${1}" + local number="${2}" + + run_quietly_if_ci echo "Configuring ScienceMesh for ${platform} ${number}" - # run db injections. - mysql_cmd="docker exec "maria${platform}${number}.docker" mariadb -u root -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek efss" - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 + local mysql_cmd="docker exec maria${platform}${number}.docker mariadb -u root -p${MARIADB_ROOT_PASSWORD} efss" + + # Insert ScienceMesh configuration into the database + $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 + $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 + $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 + $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + # Copy init files. + cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/${TEMP_DIR}/" + cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/${TEMP_DIR}/reva/configs" + cp -f "${ENV_ROOT}/docker/scripts/ocmstub/index.js" "${ENV_ROOT}/${TEMP_DIR}/index.js" + cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-sciencemesh.sh" "${ENV_ROOT}/${TEMP_DIR}/nextcloud.sh" + # Remove unnecessary configs. + rm "${ENV_ROOT}/${TEMP_DIR}/reva/configs/sciencemesh-apps-codimd.toml" + rm "${ENV_ROOT}/${TEMP_DIR}/reva/configs/sciencemesh-apps-collabora.toml" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create Nextcloud containers + # #id #username #password #init_filename #nextcloud_version + create_nextcloud 1 "einstein" "relativity" "nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 2 "michiel" "dejong" "nextcloud.sh" "${EFSS_PLATFORM_2_VERSION}" + + # Create Reva containers + create_reva "nextcloud" 1 + create_reva "nextcloud" 2 + + # Configure ScienceMesh integration + configure_sciencemesh "nextcloud" 1 + configure_sciencemesh "nextcloud" 2 + + # Start Mesh Directory + run_quietly_if_ci echo "Starting Mesh Directory..." + run_docker_container --detach --network="$DOCKER_NETWORK" \ + --name="meshdir.docker" \ + -e HOST="meshdir" \ + -v "${ENV_ROOT}/${TEMP_DIR}/index.js:/ocmstub/index.js" \ + pondersource/dev-stock-ocmstub + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://nextcloud2.docker (username: michiel, password: dejong)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/invite-link/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/configs" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/temp/" -cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/temp/reva/configs" -cp -f "${ENV_ROOT}/docker/scripts/ocmstub/index.js" "${ENV_ROOT}/temp/index.js" -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sciencemesh.sh" "${ENV_ROOT}/temp/owncloud.sh" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-sciencemesh.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# remove unnecessary configs. -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-codimd.toml" -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-collabora.toml" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# Nextclouds. -createEfss nextcloud 1 einstein relativity nextcloud.sh latest sciencemesh -createEfss nextcloud 2 michiel dejong nextcloud.sh latest sciencemesh - -############ -### Reva ### -############ - -# syntax: -# createReva platform number port. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# port: maps a port on local host to port 80 of reva, for `curl` purposes! should be unique. -# for all createReva commands, if the port is not unique or is already in use by another. -# program, script would halt! - -createReva nextcloud 1 -createReva nextcloud 2 - -################### -### ScienceMesh ### -################### - -# syntax: -# sciencemeshInsertIntoDB platform number. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. - -sciencemeshInsertIntoDB nextcloud 1 -sciencemeshInsertIntoDB nextcloud 2 - -###################### -### Mesh directory ### -###################### -docker run --detach --network=testnet \ - --name=meshdir.docker \ - -e HOST="meshdir" \ - -v "${ENV_ROOT}/temp/index.js:/ocmstub/index.js" \ - pondersource/dev-stock-ocmstub \ - >/dev/null 2>&1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://nextcloud2.docker -> username: michiel password: dejong" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From fd2a44d0993cf991533766f23317f3ffc9ac4afe Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Thu, 5 Dec 2024 07:20:44 +0000 Subject: [PATCH 010/184] modify: deprecate share to group for now --- .../9-owncloud-to-owncloud-group.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cypress/ocm-test-suite/cypress/e2e/{share-with/9-owncloud-to-owncloud-group.cy.js => deprecated/9-owncloud-to-owncloud-group.js} (100%) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/9-owncloud-to-owncloud-group.cy.js b/cypress/ocm-test-suite/cypress/e2e/deprecated/9-owncloud-to-owncloud-group.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/share-with/9-owncloud-to-owncloud-group.cy.js rename to cypress/ocm-test-suite/cypress/e2e/deprecated/9-owncloud-to-owncloud-group.js From c5209b215ac995262ddfdaf46ff51ed17b60b2eb Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Thu, 5 Dec 2024 07:22:59 +0000 Subject: [PATCH 011/184] refactor: better handling of different scenarios --- dev/ocm-test-suite.sh | 477 +++++++++++++++++++++++------------------- 1 file changed, 260 insertions(+), 217 deletions(-) diff --git a/dev/ocm-test-suite.sh b/dev/ocm-test-suite.sh index d88476f7..b4dd3d37 100755 --- a/dev/ocm-test-suite.sh +++ b/dev/ocm-test-suite.sh @@ -1,223 +1,266 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# test case: -# - login -# - share-with -# - share-link -TEST_CASE=${1:-"login"} - -# efss platform: -# - nextcloud -# - owncloud -# - seafile -EFSS_PLATFORM_1=${2:-"nextcloud"} - -EFSS_PLATFORM_1_VERSION=${3:-"unknown"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${4:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${5:-"electron"} - -# efss platform: -# - nextcloud -# - owncloud -# - seafile -EFSS_PLATFORM_2=${6:-"nextcloud"} - -EFSS_PLATFORM_2_VERSION=${7:-"unknown"} - -case "${TEST_CASE}" in - - "login") - case "${EFSS_PLATFORM_1}" in - - "nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/login/nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/login/owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "ocis") - "${ENV_ROOT}/dev/ocm-test-suite/login/ocis.sh" "${EFSS_PLATFORM_1_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "seafile") - "${ENV_ROOT}/dev/ocm-test-suite/login/seafile.sh" "${EFSS_PLATFORM_1_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "ocmstub") - "${ENV_ROOT}/dev/ocm-test-suite/login/ocmstub.sh" "${EFSS_PLATFORM_1_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - *) - echo -n "unknown login" - ;; - esac - ;; - - "share-with") - case "${EFSS_PLATFORM_1}-${EFSS_PLATFORM_2}" in - - "nextcloud-nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "nextcloud-owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "nextcloud-seafile") - echo -n "not supported" - ;; - - "nextcloud-ocmstub") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud-nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud-owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/owncloud-owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud-seafile") - echo -n "not supported" - ;; - - "owncloud-ocmstub") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "seafile-nextcloud") - echo -n "not supported" - ;; - - "seafile-owncloud") - echo -n "not supported" - ;; - - "seafile-seafile") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/seafile-seafile.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "ocmstub-nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "ocmstub-owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "ocmstub-ocmstub") - "${ENV_ROOT}/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - *) - echo -n "unknown share-with" - ;; +# ----------------------------------------------------------------------------------- +# Script to Automate EFSS OCM Test Suite Execution +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script automates EFSS (Enterprise File Synchronization and Sharing) OCM (Open Cloud Mesh) test suite case execution. +# It supports operations such as login, share-with, share-link, and invite-link for platforms like +# Nextcloud, ownCloud, Seafile, and others. + +# Usage: +# ./ocm-test-suite.sh [TEST_CASE] [EFSS_PLATFORM_1] [EFSS_PLATFORM_1_VERSION] [SCRIPT_MODE] +# [BROWSER_PLATFORM] [EFSS_PLATFORM_2] [EFSS_PLATFORM_2_VERSION] + +# Arguments: +# TEST_CASE : Test case to execute (default: "login"). +# Options: login, share-with, share-link, invite-link. +# EFSS_PLATFORM_1 : Primary EFSS platform (default: "nextcloud"). +# EFSS_PLATFORM_1_VERSION : Version of the primary EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). +# Options: chrome, edge, firefox, electron. +# EFSS_PLATFORM_2 : (Optional) Secondary EFSS platform for interop tests (default: "nextcloud"). +# EFSS_PLATFORM_2_VERSION : (Optional) Version of the secondary EFSS platform (default: "v27.1.11"). + +# TODO @MahdiBaghbani: How about more documentation? what do we exatly need to run these tests? +# Requirements: +# - Test scripts must exist in the folder structure: dev/ocm-test-suite//.sh +# - Required tools and dependencies must be installed. + +# Example: +# ./ocm-test-suite.sh login nextcloud v27.1.11 ci electron + +# Exit Codes: +# 0 - Success +# 1 - Failure or unknown test case/script. + +# ----------------------------------------------------------------------------------- + +# Exit immediately on any error, treat unset variables as an error, and catch errors in pipelines. +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "$script_dir/.." || error_exit "Failed to change directory to script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" +} + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "$message" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: run_test_script +# Purpose: Check if a test script exists and is executable, then run it with provided arguments. +# Arguments: +# $1 - The path to the test script. +# $@ - Additional arguments to pass to the test script. +# ----------------------------------------------------------------------------------- +run_test_script() { + local script_path="${1}" + shift + if [[ -x "${script_path}" ]]; then + "${script_path}" "$@" + else + error_exit "Test script not found or not executable: ${script_path}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: handle_login +# Purpose: Handle the "login" test case. +# Arguments: +# $1 - EFSS platform. +# $2 - EFSS platform version. +# $3 - Script mode. +# $4 - Browser platform. +# ----------------------------------------------------------------------------------- +handle_login() { + local platform="${1}" + local version="${2}" + local mode="${3}" + local browser="${4}" + local script_path="${ENV_ROOT}/dev/ocm-test-suite/login/${platform}.sh" + run_test_script "${script_path}" "${version}" "${mode}" "${browser}" +} + +# ----------------------------------------------------------------------------------- +# Function: handle_share_with +# Purpose: Handle the "share-with" test case. +# Arguments: +# $1 - EFSS platform 1. +# $2 - EFSS platform 1 version. +# $3 - EFSS platform 2. +# $4 - EFSS platform 2 version. +# $5 - Script mode. +# $6 - Browser platform. +# ----------------------------------------------------------------------------------- +handle_share_with() { + local platform1="${1}" + local version1="${2}" + local platform2="${3}" + local version2="${4}" + local mode="${5}" + local browser="${6}" + local script_path="${ENV_ROOT}/dev/ocm-test-suite/share-with/${platform1}-${platform2}.sh" + + # Check for unsupported combinations. + if [[ "${platform1}-${platform2}" =~ ^(nextcloud-seafile|owncloud-seafile|seafile-nextcloud|seafile-owncloud)$ ]]; then + print_error "Combination '${platform1}-${platform2}' is not supported for 'share-with' test case." + exit 1 + else + run_test_script "${script_path}" "${version1}" "${version2}" "${mode}" "${browser}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: handle_share_link +# Purpose: Handle the "share-link" test case. +# Arguments: +# $1 - EFSS platform 1. +# $2 - EFSS platform 1 version. +# $3 - EFSS platform 2. +# $4 - EFSS platform 2 version. +# $5 - Script mode. +# $6 - Browser platform. +# ----------------------------------------------------------------------------------- +handle_share_link() { + local platform1="${1}" + local version1="${2}" + local platform2="${3}" + local version2="${4}" + local mode="${5}" + local browser="${6}" + local script_path="${ENV_ROOT}/dev/ocm-test-suite/share-link/${platform1}-${platform2}.sh" + + # Check for unsupported combinations. + if [[ "${platform1}-${platform2}" =~ ^(nextcloud-seafile|owncloud-seafile|seafile-nextcloud|seafile-owncloud)$ ]]; then + print_error "Combination '${platform1}-${platform2}' is not supported for 'share-link' test case." + exit 1 + else + run_test_script "${script_path}" "${version1}" "${version2}" "${mode}" "${browser}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: handle_invite_link +# Purpose: Handle the "invite-link" test case. +# Arguments: +# $1 - EFSS platform 1. +# $2 - EFSS platform 1 version. +# $3 - EFSS platform 2. +# $4 - EFSS platform 2 version. +# $5 - Script mode. +# $6 - Browser platform. +# ----------------------------------------------------------------------------------- +handle_invite_link() { + local platform1="${1}" + local version1="${2}" + local platform2="${3}" + local version2="${4}" + local mode="${5}" + local browser="${6}" + local script_path="${ENV_ROOT}/dev/ocm-test-suite/invite-link/${platform1}-${platform2}.sh" + + # Check for unsupported combinations. + if [[ "${platform1}-${platform2}" =~ ^(nextcloud-seafile|owncloud-seafile|seafile-nextcloud|seafile-owncloud)$ ]]; then + print_error "Combination '${platform1}-${platform2}' is not supported for 'invite-link' test case." + exit 1 + else + run_test_script "${script_path}" "${version1}" "${version2}" "${mode}" "${browser}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: main +# Purpose: Main function to manage the flow of the script. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment. + initialize_environment + + # Parse arguments with default values. + local test_case="${1:-login}" + local efss_platform_1="${2:-nextcloud}" + local efss_platform_1_version="${3:-unknown}" + local script_mode="${4:-dev}" + local browser_platform="${5:-electron}" + local efss_platform_2="${6:-nextcloud}" + local efss_platform_2_version="${7:-unknown}" + + # Validate test case. + case "${test_case}" in + "login"|"share-with"|"share-link"|"invite-link") + ;; + *) + error_exit "Unknown test case: '${test_case}'. Valid options are: login, share-with, share-link, invite-link." + ;; esac - ;; - - "share-link") - case "${EFSS_PLATFORM_1}-${EFSS_PLATFORM_2}" in - - "nextcloud-nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "nextcloud-owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud-nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud-owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/share-link/owncloud-owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - *) - echo -n "unknown share-link" - ;; - esac - ;; - - "invite-link") - case "${EFSS_PLATFORM_1}-${EFSS_PLATFORM_2}" in - - "nextcloud-nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "nextcloud-owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "nextcloud-ocis") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/nextcloud-ocis.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud-nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud-owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/owncloud-owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "owncloud-ocis") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/owncloud-ocis.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "ocis-ocis") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/ocis-ocis.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "ocis-owncloud") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/ocis-owncloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "ocis-nextcloud") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/ocis-nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - - "cernbox-cernbox") - "${ENV_ROOT}/dev/ocm-test-suite/invite-link/cernbox-cernbox.sh" "${EFSS_PLATFORM_1_VERSION}" "${EFSS_PLATFORM_2_VERSION}" "${SCRIPT_MODE}" "${BROWSER_PLATFORM}" - ;; - *) - echo -n "unknown invite-link" - ;; + # Route the test case to the appropriate handler. + case "$test_case" in + "login") + handle_login "${efss_platform_1}" "${efss_platform_1_version}" "${script_mode}" "${browser_platform}" + ;; + "share-with") + handle_share_with "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" + ;; + "share-link") + handle_share_link "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" + ;; + "invite-link") + handle_invite_link "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" + ;; esac - ;; +} - *) - echo -n "unknown" - ;; -esac +# ----------------------------------------------------------------------------------- +# Execute the main function and pass all script arguments. +# ----------------------------------------------------------------------------------- +main "$@" From 3a90d3c33a83daaf5fad38a26869905576d35888 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 9 Dec 2024 08:51:55 +0000 Subject: [PATCH 012/184] [no ci] refactor: better documentation and fucntions for cypress tests --- .../nextcloud-v27-to-nextcloud-v27.cy.js | 147 +++-- .../nextcloud-v27-to-owncloud-v10.cy.js | 182 ++++-- .../owncloud-v10-to-nextcloud-v27.cy.js | 186 ++++-- .../owncloud-v10-to-owncloud-v10.cy.js | 150 +++-- .../cypress/e2e/login/nextcloud.cy.js | 27 +- .../cypress/e2e/login/ocmstub.cy.js | 27 +- .../cypress/e2e/login/owncloud.cy.js | 27 +- .../nextcloud-v27-to-nextcloud-v27.cy.js | 97 ++- .../nextcloud-v27-to-nextcloud-v28.cy.js | 87 ++- .../nextcloud-v27-to-ocmstub-v1.cy.js | 108 +++- .../nextcloud-v27-to-owncloud-v10.cy.js | 86 ++- .../nextcloud-v28-to-nextcloud-v27.cy.js | 92 ++- .../nextcloud-v28-to-nextcloud-v28.cy.js | 85 ++- .../nextcloud-v28-to-ocmstub-v1.cy.js | 115 +++- .../nextcloud-v28-to-owncloud-v10.cy.js | 86 ++- .../nextcloud-v29-to-ocmstub-v1.cy.js | 81 +++ .../nextcloud-v30-to-ocmstub-v1.cy.js | 80 +++ .../share-with/ocmstub-v1-to-ocmstub-v1.cy.js | 83 ++- .../owncloud-v10-to-nextcloud-v27.cy.js | 100 +++- .../owncloud-v10-to-nextcloud-v28.cy.js | 85 ++- .../owncloud-v10-to-ocmstub-v1.cy.js | 105 +++- .../owncloud-v10-to-owncloud-v10.cy.js | 79 ++- .../cypress/e2e/utils/general.js | 18 + .../cypress/e2e/utils/nextcloud-v27.js | 562 ++++++++++++------ .../cypress/e2e/utils/nextcloud-v28.js | 310 +++++++--- .../cypress/e2e/utils/ocmstub-v1.js | 82 +++ .../cypress/e2e/utils/owncloud.js | 495 +++++++++++---- 27 files changed, 2685 insertions(+), 897 deletions(-) create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js create mode 100644 cypress/ocm-test-suite/cypress/e2e/utils/general.js create mode 100644 cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-nextcloud-v27.cy.js index 13fba894..ca44bae8 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-nextcloud-v27.cy.js @@ -1,64 +1,131 @@ +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality in Nextcloud v27. + * This suite covers sending and accepting invitation links, sharing files via ScienceMesh, + * and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { + acceptShareV27, createInviteLinkV27, verifyFederatedContactV27, + acceptScienceMeshInvitation, createScienceMeshShareV27, renameFileV27, + ensureFileExistsV27, navigationSwitchLeftSideV27, selectAppFromLeftSideV27, -} from '../utils/nextcloud-v27' +} from '../utils/nextcloud-v27'; describe('Invite link federated sharing via ScienceMesh functionality for Nextcloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD2_URL') || 'https://nextcloud2.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('NEXTCLOUD2_USERNAME') || 'michiel'; + const recipientPassword = Cypress.env('NEXTCLOUD2_PASSWORD') || 'dejong'; + const inviteLinkFileName = 'invite-link-nc-nc.txt'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'invite-link-nc-nc.txt'; + + /** + * Test case: Sending an invitation link from sender to recipient. + */ it('Send invitation from Nextcloud v27 to Nextcloud v27', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - cy.visit('https://nextcloud1.docker/index.php/apps/sciencemesh/contacts') + // Step 2: Navigate to the ScienceMesh app + cy.visit(`${senderUrl}/index.php/apps/sciencemesh/contacts`); - createInviteLinkV27('https://nextcloud2.docker').then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-nc-nc.txt', result) - } - ) - }) + // Step 3: Generate the invite link and save it to a file + createInviteLinkV27(recipientUrl).then((inviteLink) => { + // Step 4: Ensure the invite link is not empty + expect(inviteLink).to.be.a('string').and.not.be.empty; + // Step 5: Save the invite link to a file for later use + cy.writeFile(inviteLinkFileName, inviteLink); + }); + }); + /** + * Test case: Accepting the invitation link on the recipient's side. + */ it('Accept invitation from Nextcloud v27 to Nextcloud v27', () => { + const expectedContactDisplayName = senderUsername; + // Extract domain without protocol or trailing slash + // Note: The 'reva' prefix is added to the expected contact domain as per application behavior + const expectedContactDomain = `reva${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`; + + // Step 1: Load the invite link from the saved file + cy.readFile(inviteLinkFileName).then((inviteLink) => { + // Step 2: Ensure the invite link is valid + expect(inviteLink).to.be.a('string').and.not.be.empty; + + // Step 3: Login to the recipient's Nextcloud instance using the invite link + cy.loginNextcloudCore(inviteLink, recipientUsername, recipientPassword); + + // Step 4: Accept the invitation + acceptScienceMeshInvitation(); + + // Step 5: Verify that the sender is now a contact in the recipient's contacts list + verifyFederatedContactV27( + recipientUrl.replace(/^https?:\/\/|\/$/g, ''), + expectedContactDisplayName, + expectedContactDomain + ); + }); + }); + + /** + * Test case: Sharing a file via ScienceMesh from sender to recipient. + */ + it('Send ScienceMesh share of a file from Nextcloud v27 to Nextcloud v27', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); - // load invite link from file. - cy.readFile('invite-link-nc-nc.txt').then((url) => { + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV27(originalFileName); - // accept invitation from Nextcloud 2. - cy.loginNextcloudCore(url, 'michiel', 'dejong') + // Step 3: Rename the file to prepare it for sharing + renameFileV27(originalFileName, sharedFileName); - cy.get('input[id="accept-button"]', { timeout: 10000 }) - .click() + // Step 4: Verify the file has been renamed + ensureFileExistsV27(sharedFileName); - // validate 'eisntein' is shown as a contact. - verifyFederatedContactV27('nextcloud2.docker', 'einstein', 'revanextcloud1.docker') - }) - }) + // Step 5: Create a federated share for the recipient via ScienceMesh + // Note: The 'reva' prefix is added to the recipient domain as per application behavior + createScienceMeshShareV27( + senderUrl.replace(/^https?:\/\/|\/$/g, ''), + recipientUsername, + `reva${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + sharedFileName + ); - it('Send ScienceMesh share from Nextcloud v27 to Nextcloud v27', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); - renameFileV27('welcome.txt', 'invite-link-nc-nc.txt') - createScienceMeshShareV27('nextcloud1.docker', 'michiel', 'revanextcloud2.docker', 'invite-link-nc-nc.txt') - }) + /** + * Test case: Receiving and verifying the ScienceMesh share on the recipient's side. + */ + it('Receive ScienceMesh share of a file from Nextcloud v27 to Nextcloud v27', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); - it('Receive ScienceMesh share from Nextcloud v27 to Nextcloud v27', () => { - // accept share from Nextcloud 2. - cy.loginNextcloud('https://nextcloud2.docker', 'michiel', 'dejong') + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV27(); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Step 3: Navigate to the correct section + navigationSwitchLeftSideV27('Open navigation'); + selectAppFromLeftSideV27('files'); + navigationSwitchLeftSideV27('Close navigation'); - navigationSwitchLeftSideV27('Open navigation') - selectAppFromLeftSideV27('shareoverview') - navigationSwitchLeftSideV27('Close navigation') + // Step 4: Verify the shared file is visible + ensureFileExistsV27(sharedFileName); - cy.get('[data-file="invite-link-nc-nc.txt"]', { timeout: 10000 }).should('be.visible') - }) -}) + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-owncloud-v10.cy.js index c359d08d..72bfe3e2 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-owncloud-v10.cy.js @@ -1,67 +1,133 @@ +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality in Nextcloud v27 and ownCloud v10. + * This suite covers sending and accepting invitation links, sharing files via ScienceMesh, + * and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { createInviteLinkV27, createScienceMeshShareV27, - renameFileV27 -} from '../utils/nextcloud-v27' + renameFileV27, + ensureFileExistsV27, +} from '../utils/nextcloud-v27'; import { - selectAppFromLeftSide -} from '../utils/owncloud' + acceptShare, + verifyFederatedContact, + acceptScienceMeshInvitation, + ensureFileExists, + selectAppFromLeftSide, +} from '../utils/owncloud'; describe('Invite link federated sharing via ScienceMesh functionality for Nextcloud to ownCloud', () => { - it('Send invitation from Nextcloud v27 to ownCloud v10', () => { - - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - cy.visit('https://nextcloud1.docker/index.php/apps/sciencemesh/contacts') - - createInviteLinkV27('https://owncloud1.docker').then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-nc-oc.txt', result) - } - ) - }) - - it('Accept invitation from Nextcloud v27 to ownCloud v10', () => { - - // load invite link from file. - cy.readFile('invite-link-nc-oc.txt').then((url) => { - - // accept invitation from Nextcloud 1. - cy.loginOwncloudCore(url, 'marie', 'radioactivity') - - cy.get('input[id="accept-button"]', { timeout: 10000 }) - .click() - // validate 'einstein' is shown as a contact. - cy.visit('https://owncloud1.docker/index.php/apps/sciencemesh/contacts') - - cy.get('table[id="contact-table"]') - .find('p[class="displayname"]') - .should("have.text", "einstein"); - }) - }) - - it('Send ScienceMesh share from Nextcloud v27 to ownCloud v10', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - - renameFileV27('welcome.txt', 'invite-link-nc-oc.txt') - createScienceMeshShareV27('nextcloud1.docker', 'marie', 'revaowncloud1.docker', 'invite-link-nc-oc.txt') - }) - - it('Receive ScienceMesh share from Nextcloud v27 to ownCloud v10', () => { - // accept share Nextcloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() - - selectAppFromLeftSide('sharingin') - - cy.get('[data-file="invite-link-nc-oc.txt"]', { timeout: 10000 }).should('be.visible') - }) + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const recipientPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const inviteLinkFileName = 'invite-link-nc-oc.txt'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'invite-link-nc-oc.txt'; + + /** + * Test case: Sending an invitation link from sender to recipient. + */ + it('Send invitation from Nextcloud v27 to ownCloud v10', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Navigate to the ScienceMesh app + cy.visit(`${senderUrl}/index.php/apps/sciencemesh/contacts`); + + // Step 3: Generate the invite link and save it to a file + createInviteLinkV27(recipientUrl).then((inviteLink) => { + // Step 4: Ensure the invite link is not empty + expect(inviteLink).to.be.a('string').and.not.be.empty; + // Step 5: Save the invite link to a file for later use + cy.writeFile(inviteLinkFileName, inviteLink); + }); + }); + + /** + * Test case: Accepting the invitation link on the recipient's side. + */ + it('Accept invitation from from Nextcloud v27 v10 to ownCloud v10', () => { + const expectedContactDisplayName = senderUsername; + // Extract domain without protocol or trailing slash + // Note: The 'reva' prefix is added to the expected contact domain as per application behavior + const expectedContactDomain = `reva${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`; + + // Step 1: Read the invite link from the file + cy.readFile(inviteLinkFileName).then((inviteLink) => { + // Step 2: Ensure the invite link is valid + expect(inviteLink).to.be.a('string').and.not.be.empty; + + // Step 3: Login to the recipient's ownCloud instance using the invite link + cy.loginOwncloudCore(inviteLink, recipientUsername, recipientPassword); + + // Step 4: Accept the invitation + acceptScienceMeshInvitation(); + + // Step 5: Verify that the sender is now a contact in the recipient's contacts list + verifyFederatedContact( + recipientUrl.replace(/^https?:\/\/|\/$/g, ''), + expectedContactDisplayName, + expectedContactDomain + ); + }); + }); + + /** + * Test case: Sharing a file via ScienceMesh from sender to recipient. + */ + it('Send ScienceMesh share of a file from Nextcloud v27 to ownCloud v10', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV27(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV27(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV27(sharedFileName); + + // Step 5: Create a federated share for the recipient via ScienceMesh + // Note: The 'reva' prefix is added to the recipient domain as per application behavior + createScienceMeshShareV27( + senderUrl.replace(/^https?:\/\/|\/$/g, ''), + recipientUsername, + `reva${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + sharedFileName + ); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving and accepting a ScienceMesh file share on ownCloud. + * This test verifies that the shared file appears in the "Sharing In" section. + */ + it('Receive ScienceMesh share of a file from Nextcloud v27 to ownCloud v10', () => { + // Step 1: Log in to the recipient's ownCloud instance + cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShare(); + + // Step 3: Navigate to the correct section + selectAppFromLeftSide('files'); + + // Step 4: Verify that the shared file is visible + ensureFileExists(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-nextcloud-v27.cy.js index 29d6befd..13248268 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-nextcloud-v27.cy.js @@ -1,70 +1,134 @@ -import { - createInviteLink, - createScienceMeshShare, - renameFile -} from '../utils/owncloud' +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality in ownCloud v10 and Nextcloud v27. + * This suite covers sending and accepting invitation links, sharing files via ScienceMesh, + * and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ import { + acceptShareV27, + verifyFederatedContactV27, + acceptScienceMeshInvitation, + ensureFileExistsV27, navigationSwitchLeftSideV27, selectAppFromLeftSideV27, -} from '../utils/nextcloud-v27' - -describe('Invite link federated sharing via ScienceMesh functionality for ownCloud to Nextcloud', () => { - it('Send invitation from ownCloud v10 to Nextcloud v27', () => { +} from '../utils/nextcloud-v27'; - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - cy.visit('https://owncloud1.docker/index.php/apps/sciencemesh/') +import { + createInviteLink, + createScienceMeshShare, + renameFile, + ensureFileExists, +} from '../utils/owncloud'; - createInviteLink('https://nextcloud1.docker').then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-oc-nc.txt', result) - } - ) - }) +describe('Invite link federated sharing via ScienceMesh functionality for ownCloud to Nextcloud', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const senderUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const senderPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const inviteLinkFileName = 'invite-link-oc-nc.txt'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'invite-link-oc-nc.txt'; + + /** + * Test case: Sending an invitation link from sender to recipient. + */ + it('Send invitation from from ownCloud v10 to Nextcloud v27', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Navigate to the ScienceMesh app + cy.visit(`${senderUrl}/index.php/apps/sciencemesh/`); + + // Step 3: Generate an invite link and save it to a file + createInviteLink(recipientUrl).then((inviteLink) => { + // Step 4: Ensure the invite link is not empty + expect(inviteLink).to.be.a('string').and.not.be.empty; + // Step 5: Save the invite link to a file for later use + cy.writeFile(inviteLinkFileName, inviteLink); + }); + }); + + /** + * Test case: Accepting the invitation link on the recipient's side. + */ it('Accept invitation from ownCloud v10 to Nextcloud v27', () => { - - // load invite link from file. - cy.readFile('invite-link-oc-nc.txt').then((url) => { - - // accept invitation from Nextcloud 1. - cy.loginNextcloudCore(url, 'einstein', 'relativity') - - cy.get('input[id="accept-button"]', { timeout: 10000 }) - .click() - - // validate 'einstein' is shown as a contact. - cy.visit('https://nextcloud1.docker/index.php/apps/sciencemesh/contacts') - - cy.get('table[id="contact-table"]') - .find('p[class="displayname"]') - .should("have.text", "marie"); - }) - }) - - it('Send ScienceMesh share from ownCloud v10 to Nextcloud v27', () => { - // share from ownCloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - - renameFile('welcome.txt', 'oc1-to-nc1-sciencemesh-share.txt') - createScienceMeshShare('oc1-to-nc1-sciencemesh-share.txt', 'einstein', 'revanextcloud1.docker') - }) - - it('Receive ScienceMesh share from ownCloud v10 to Nextcloud v27', () => { - // accept share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() - - navigationSwitchLeftSideV27('Open navigation') - selectAppFromLeftSideV27('shareoverview') - navigationSwitchLeftSideV27('Close navigation') - - cy.get('[data-file="oc1-to-nc1-sciencemesh-share.txt"]', { timeout: 10000 }).should('be.visible') - }) + const expectedContactDisplayName = senderUsername; + // Extract domain without protocol or trailing slash + // Note: The 'reva' prefix is added to the expected contact domain as per application behavior + const expectedContactDomain = `reva${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`; + + // Step 1: Load the invite link from the saved file + cy.readFile(inviteLinkFileName).then((inviteLink) => { + // Step 2: Ensure the invite link is valid + expect(inviteLink).to.be.a('string').and.not.be.empty; + + // Step 3: Login to the recipient's Nextcloud instance using the invite link + cy.loginNextcloudCore(inviteLink, recipientUsername, recipientPassword); + + // Step 4: Accept the invitation + acceptScienceMeshInvitation(); + + // Step 5: Verify that the sender is now a contact in the recipient's contacts list + verifyFederatedContactV27( + recipientUrl.replace(/^https?:\/\/|\/$/g, ''), + expectedContactDisplayName, + expectedContactDomain + ); + }); + }); + + /** + * Test case: Sharing a file via ScienceMesh from sender to recipient. + */ + it('Send ScienceMesh share of a file from ownCloud v10 to Nextcloud v27', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists + ensureFileExists(originalFileName); + + // Step 3: Rename the file + renameFile(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExists(sharedFileName); + + // Step 5: Create a federated share for the recipient via ScienceMesh + // Note: The 'reva' prefix is added to the recipient domain as per application behavior + createScienceMeshShare( + sharedFileName, + recipientUsername, + `reva${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + ); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test case: Receiving and verifying the ScienceMesh share on the recipient's side. + */ + it('Receive ScienceMesh share of a file from ownCloud v10 to Nextcloud v27', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV27(); + + // Step 3: Navigate to the correct section + navigationSwitchLeftSideV27('Open navigation'); + selectAppFromLeftSideV27('files'); + navigationSwitchLeftSideV27('Close navigation'); + + // Step 4: Verify the shared file is visible + ensureFileExistsV27(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-owncloud-v10.cy.js index 5a4ac867..9cdac5d2 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-owncloud-v10.cy.js @@ -1,64 +1,128 @@ +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality in ownCloud v10. + * This suite covers sending and accepting invitation links, sharing files via ScienceMesh, + * and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { + acceptShare, createInviteLink, + verifyFederatedContact, + acceptScienceMeshInvitation, createScienceMeshShare, renameFile, + ensureFileExists, selectAppFromLeftSide, -} from '../utils/owncloud' +} from '../utils/owncloud'; describe('Invite link federated sharing via ScienceMesh functionality for ownCloud', () => { - it('Send invitation from ownCloud v10 to ownCloud v10', () => { - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - cy.visit('https://owncloud1.docker/index.php/apps/sciencemesh/') + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUrl = Cypress.env('OWNCLOUD2_URL') || 'https://owncloud2.docker'; + const senderUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const senderPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const recipientUsername = Cypress.env('OWNCLOUD2_USERNAME') || 'mahdi'; + const recipientPassword = Cypress.env('OWNCLOUD2_PASSWORD') || 'baghbani'; + const inviteLinkFileName = 'invite-link-oc-oc.txt'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'invite-link-oc-oc.txt'; + + /** + * Test case: Sending an invitation link from sender to recipient. + */ + it('Send invitation from from ownCloud v10 to ownCloud v10', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Navigate to the ScienceMesh app + cy.visit(`${senderUrl}/index.php/apps/sciencemesh/`); + + // Step 3: Generate an invite link and save it to a file + createInviteLink(recipientUrl).then((inviteLink) => { + // Step 4: Ensure the invite link is not empty + expect(inviteLink).to.be.a('string').and.not.be.empty; + // Step 5: Save the invite link to a file for later use + cy.writeFile(inviteLinkFileName, inviteLink); + }); + }); + + /** + * Test case: Accepting the invitation link on the recipient's side. + */ + it('Accept invitation from from ownCloud v10 to ownCloud v10', () => { + const expectedContactDisplayName = senderUsername; + // Extract domain without protocol or trailing slash + // Note: The 'reva' prefix is added to the expected contact domain as per application behavior + const expectedContactDomain = `reva${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`; + + // Step 1: Read the invite link from the file + cy.readFile(inviteLinkFileName).then((inviteLink) => { + // Step 2: Ensure the invite link is valid + expect(inviteLink).to.be.a('string').and.not.be.empty; + + // Step 3: Login to the recipient's ownCloud instance using the invite link + cy.loginOwncloudCore(inviteLink, recipientUsername, recipientPassword); - createInviteLink('https://owncloud2.docker').then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-oc-oc.txt', result) - } - ) - }) + // Step 4: Accept the invitation + acceptScienceMeshInvitation(); - it('Accept invitation from ownCloud v10 to ownCloud v10', () => { + // Step 5: Verify that the sender is now a contact in the recipient's contacts list + verifyFederatedContact( + recipientUrl.replace(/^https?:\/\/|\/$/g, ''), + expectedContactDisplayName, + expectedContactDomain + ); + }); + }); - // load invite link from file. - cy.readFile('invite-link-oc-oc.txt').then((url) => { + /** + * Test case: Sharing a file via ScienceMesh from sender to recipient. + */ + it('Send ScienceMesh share of a file from ownCloud v10 to ownCloud v10', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); - // accept invitation from ownCloud 2. - cy.loginOwncloudCore(url, 'mahdi', 'baghbani') + // Step 2: Ensure the original file exists + ensureFileExists(originalFileName); - cy.get('input[id="accept-button"]', { timeout: 10000 }) - .click() + // Step 3: Rename the file + renameFile(originalFileName, sharedFileName); - // validate 'marie' is shown as a contact. - cy.visit('https://owncloud2.docker/index.php/apps/sciencemesh/contacts') + // Step 4: Verify the file has been renamed + ensureFileExists(sharedFileName); - cy.get('table[id="contact-table"]') - .find('p[class="displayname"]') - .should("have.text", "marie"); - }) - }) + // Step 5: Create a federated share for the recipient via ScienceMesh + // Note: The 'reva' prefix is added to the recipient domain as per application behavior + createScienceMeshShare( + sharedFileName, + recipientUsername, + `reva${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + ); - it('Send ScienceMesh share from ownCloud v10 to ownCloud v10', () => { - // share from ownCloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); - renameFile('welcome.txt', 'oc1-to-oc2-sciencemesh-share.txt') - createScienceMeshShare('oc1-to-oc2-sciencemesh-share.txt', 'mahdi', 'revaowncloud2.docker') - }) + /** + * Test Case: Receiving and accepting a ScienceMesh file share on ownCloud 2. + * This test verifies that the shared file appears in the "Sharing In" section. + */ + it('Receive ScienceMesh share of a file from ownCloud v10 to ownCloud v10', () => { + // Step 1: Log in to the recipient's ownCloud instance + cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); - it('Receive ScienceMesh share from ownCloud v10 to ownCloud v10', () => { - // accept share from ownCloud 2. - cy.loginOwncloud('https://owncloud2.docker', 'mahdi', 'baghbani') + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShare(); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Step 3: Navigate to the correct section + selectAppFromLeftSide('files'); - selectAppFromLeftSide('sharingin') + // Step 4: Verify that the shared file is visible + ensureFileExists(sharedFileName); - cy.get('[data-file="oc1-to-oc2-sciencemesh-share.txt"]', { timeout: 10000 }).should('be.visible') - }) -}) + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/login/nextcloud.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/nextcloud.cy.js index 8a041dd3..213485dc 100644 --- a/cypress/ocm-test-suite/cypress/e2e/login/nextcloud.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/login/nextcloud.cy.js @@ -1,5 +1,22 @@ -describe('Login Nextcloud', () => { - it('Login test for Nextcloud', () => { - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - }) -}) +/** + * @fileoverview + * Cypress test suite for testing the login functionality of Nextcloud. + * This suite contains tests to validate successful login functionality using valid credentials. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +describe('Nextcloud Login Tests', () => { + /** + * Test Case: Validates successful login to Nextcloud. + * This test logs into Nextcloud using valid credentials and checks for a successful login state. + */ + it('should successfully log into Nextcloud with valid credentials', () => { + // Define the Nextcloud instance URL and credentials from environment variables or use default values + const nextcloudUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const username = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const password = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + + cy.loginNextcloud(nextcloudUrl, username, password); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/login/ocmstub.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/ocmstub.cy.js index efde4323..dd888c19 100644 --- a/cypress/ocm-test-suite/cypress/e2e/login/ocmstub.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/login/ocmstub.cy.js @@ -1,5 +1,22 @@ -describe('Login OcmStub', () => { - it('Login test for OcmStub', () => { - cy.loginOcmStub('https://ocmstub1.docker/?') - }) -}) +/** + * @fileoverview + * Cypress test suite for testing the login functionality of OcmStub. + * This suite contains tests to validate successful login functionality using valid credentials. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +describe('OcmStub Login Tests', () => { + /** + * Test Case: Validates successful login to OcmStub. + * This test logs into OcmStub using valid credentials and checks for a successful login state. + */ + it('should successfully log into OcmStub with valid credentials', () => { + // Define the OcmStub instance URL and credentials from environment variables or use default values + const ocmstubUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const username = Cypress.env('OCMSTUB1_USERNAME') || 'einstein'; + const password = Cypress.env('OCMSTUB1_PASSWORD') || 'relativity'; + + cy.loginOcmStub('https://ocmstub1.docker/?'); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/login/owncloud.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/owncloud.cy.js index 96988e02..9ab37b41 100644 --- a/cypress/ocm-test-suite/cypress/e2e/login/owncloud.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/login/owncloud.cy.js @@ -1,5 +1,22 @@ -describe('Login ownCloud', () => { - it('Login test for ownCloud', () => { - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - }) -}) +/** + * @fileoverview + * Cypress test suite for testing the login functionality of ownCloud. + * This suite contains tests to validate successful login functionality using valid credentials. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +describe('ownCloud Login Tests', () => { + /** + * Test Case: Validates successful login to ownCloud. + * This test logs into ownCloud using valid credentials and checks for a successful login state. + */ + it('should successfully log into ownCloud with valid credentials', () => { + // Define the ownCloud instance URL and credentials from environment variables or use default values + const owncloudUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const username = Cypress.env('OWNCLOUD1_USERNAME') || 'einstein'; + const password = Cypress.env('OWNCLOUD1_PASSWORD') || 'relativity'; + + cy.loginOwncloud(owncloudUrl, username, password); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v27.cy.js index e5813818..962b22a8 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v27.cy.js @@ -1,33 +1,74 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v27. + * This suite verifies the ability to send and receive federated file shares between two Nextcloud instances. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { + acceptShareV27, createShareV27, renameFileV27, + ensureFileExistsV27, navigationSwitchLeftSideV27, selectAppFromLeftSideV27, -} from '../utils/nextcloud-v27' - -describe('Native federated sharing functionality for Nextcloud', () => { - it('Send federated share from Nextcloud v27 to Nextcloud v27', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - - renameFileV27('welcome.txt', 'nc1-to-nc2-share.txt') - createShareV27('nc1-to-nc2-share.txt', 'michiel', 'nextcloud2.docker') - }) - - it('Receive federated share from Nextcloud v27 to Nextcloud v27', () => { - // accept share from Nextcloud 2. - cy.loginNextcloud('https://nextcloud2.docker', 'michiel', 'dejong') - - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() - - navigationSwitchLeftSideV27('Open navigation') - selectAppFromLeftSideV27('shareoverview') - navigationSwitchLeftSideV27('Close navigation') - - cy.get('[data-file="nc1-to-nc2-share.txt"]', { timeout: 10000 }).should('be.visible') - }) -}) +} from '../utils/nextcloud-v27'; + +describe('Native Federated Sharing Functionality for Nextcloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD2_URL') || 'https://nextcloud2.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('NEXTCLOUD2_USERNAME') || 'michiel'; + const recipientPassword = Cypress.env('NEXTCLOUD2_PASSWORD') || 'dejong'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-nc1-to-nc2.txt'; + + /** + * Test Case: Sending a federated share from one Nextcloud instance to another. + * Validates that a file can be successfully shared from Nextcloud Instance 1 to Nextcloud Instance 2. + */ + it('Send a federated share of a file from Nextcloud v27 to Nextcloud v27', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV27(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV27(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV27(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV27(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from Nextcloud v27 to Nextcloud v27', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV27(); + + // Step 3: Navigate to the correct section + navigationSwitchLeftSideV27('Open navigation'); + selectAppFromLeftSideV27('files'); + navigationSwitchLeftSideV27('Close navigation'); + + // Step 4: Verify the shared file is visible + ensureFileExistsV27(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v28.cy.js index fadeac4a..d2adf7b0 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v28.cy.js @@ -1,27 +1,74 @@ -import { createShareV27, renameFileV27 } from '../utils/nextcloud-v27' +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v27 and v28. + * This suite verifies the ability to send and receive federated file shares between two Nextcloud instances with different versions. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ -describe('Native federated sharing functionality for Nextcloud', () => { - it('Send federated share from Nextcloud v27 to Nextcloud v28', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') +import { + createShareV27, + renameFileV27, + ensureFileExistsV27, +} from '../utils/nextcloud-v27'; - renameFileV27('welcome.txt', 'nc1-to-nc2-share.txt') - createShareV27('nc1-to-nc2-share.txt', 'michiel', 'nextcloud2.docker') - }) +import { + acceptShareV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; - it('Receive federated share from Nextcloud v27 to Nextcloud v28', () => { - // accept share from Nextcloud 2. - cy.loginNextcloud('https://nextcloud2.docker', 'michiel', 'dejong') +describe('Native Federated Sharing Functionality for Nextcloud', () => { - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD2_URL') || 'https://nextcloud2.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('NEXTCLOUD2_USERNAME') || 'michiel'; + const recipientPassword = Cypress.env('NEXTCLOUD2_PASSWORD') || 'dejong'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-nc1-to-nc2.txt'; - // force reload the page for share to apear. - cy.reload(true) + /** + * Test Case: Sending a federated share from one Nextcloud instance to another. + * Validates that a file can be successfully shared from Nextcloud Instance 1 to Nextcloud Instance 2. + */ + it('Send a federated share of a file from Nextcloud v27 to Nextcloud v28', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); - cy.get('[data-cy-files-list-row-name="nc1-to-nc2-share.txt"]', { timeout: 10000 }).should('be.visible') - }) + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV27(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV27(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV27(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV27(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from Nextcloud v27 to Nextcloud v28', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV28(); + + // Step 3: Reload the page to ensure the shared file appears in the file list + cy.reload(true); + + // Step 4: Verify the shared file is visible + ensureFileExistsV28(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js index 7b39bc8d..d58c63ca 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js @@ -1,30 +1,84 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v27 and OcmStub v1. + * + * @author Michiel De Jong + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { createShareV27, renameFileV27, -} from '../utils/nextcloud-v27' - -describe('Native federated sharing functionality for Nextcloud', () => { - it('Send federated share from Nextcloud v27 to OcmStub v1.0.0', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - - renameFileV27('welcome.txt', 'nc1-to-os1-share.txt') - createShareV27('nc1-to-os1-share.txt', 'michiel', 'ocmstub1.docker') - }) - - it('Receive federated share from Nextcloud v27 to OcmStub v1.0.0', () => { - // accept share from OcmStub 1. - cy.loginOcmStub('https://ocmstub1.docker/?') - - cy.contains('"shareWith": "michiel@https://ocmstub1.docker"').should('be.visible') - cy.contains('"shareType": "user"').should('be.visible') - cy.contains('"name": "nc1-to-os1-share.txt"').should('be.visible') - cy.contains('"resourceType": "file"').should('be.visible') - cy.contains('"owner": "einstein@https://nextcloud1.docker/"').should('be.visible') - cy.contains('"sharedBy": "einstein@https://nextcloud1.docker/"').should('be.visible') - cy.contains('"ownerDisplayName": "einstein"').should('be.visible') - cy.contains('"description": ""').should('be.visible') - cy.contains('"protocol": { "name": "webdav", "options": { "sharedSecret": "').should('be.visible') - cy.contains('"permissions": "{http://open-cloud-mesh.org/ns}share-permissions"').should('be.visible') - }) -}) + ensureFileExistsV27, +} from '../utils/nextcloud-v27'; + +import { + generateShareAssertions, +} from '../utils/ocmstub-v1.js'; + +describe('Native Federated Sharing Functionality for Nextcloud to OcmStub', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'nc1-to-os1-share.txt'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}/`, + sender: `${senderUsername}@${senderUrl}/`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + + /** + * Test Case: Sending a federated share from one Nextcloud instance to OcmStub. + * Validates that a file can be successfully shared from Nextcloud to OcmStub. + */ + it('Send a federated share of a file from Nextcloud v27 to OcmStub v1', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV27(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV27(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV27(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV27(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving a federated share on OcmStub from Nextcloud. + */ + it('Receive federated share of a file from from Nextcloud v27 to OcmStub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js index 51c3b83e..308b1f77 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js @@ -1,33 +1,75 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v27 and ownCloud v10. + * This suite verifies the ability to send and receive federated file shares between Nextcloud and ownCloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { createShareV27, - renameFileV27 -} from '../utils/nextcloud-v27' + renameFileV27, + ensureFileExistsV27, +} from '../utils/nextcloud-v27'; import { - selectAppFromLeftSide -} from '../utils/owncloud' + acceptShare, + ensureFileExists, + selectAppFromLeftSide, +} from '../utils/owncloud'; + +describe('Native Federated Sharing Functionality for Nextcloud to ownCloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const recipientPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-nc1-to-oc1.txt'; + + /** + * Test Case: Sending a federated share from one Nextcloud instance to ownCloud. + * Validates that a file can be successfully shared from Nextcloud to ownCloud. + */ + it('Send a federated share of a file from Nextcloud v27 to ownCloud v10', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV27(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV27(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV27(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV27(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); -describe('OCM federated sharing functionality for Nextcloud', () => { - it('Send federated share from Nextcloud v27 to ownCloud v10', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); - renameFileV27('welcome.txt', 'nc1-to-oc1-share.txt') - createShareV27('nc1-to-oc1-share.txt', 'marie', 'owncloud1.docker') - }) + /** + * Test Case: Receiving and accepting a federated share on the recipient's ownCloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from Nextcloud v27 to ownCloud v10', () => { + // Step 1: Log in to the recipient's ownCloud instance + cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); - it('Receive federated share from Nextcloud v27 to ownCloud v10', () => { - // accept share from Nextcloud 2. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShare(); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Step 3: Navigate to the correct section + selectAppFromLeftSide('files'); - selectAppFromLeftSide('sharingin') + // Step 4: Verify that the shared file is visible + ensureFileExists(sharedFileName); - cy.get('[data-file="nc1-to-oc1-share.txt"]', { timeout: 10000 }).should('be.visible') - }) + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-nextcloud-v27.cy.js index c2bb7d7b..6c41bb01 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-nextcloud-v27.cy.js @@ -1,36 +1,78 @@ -import { - createShareV28, - renameFileV28 -} from '../utils/nextcloud-v28' +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v28 and v27. + * This suite verifies the ability to send and receive federated file shares between two Nextcloud instances with different versions. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ import { + acceptShareV27, + ensureFileExistsV27, navigationSwitchLeftSideV27, selectAppFromLeftSideV27, -} from '../utils/nextcloud-v27' +} from '../utils/nextcloud-v27'; + +import { + createShareV28, + renameFileV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; + +describe('Native Federated Sharing Functionality for Nextcloud v28 and v27', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD2_URL') || 'https://nextcloud2.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('NEXTCLOUD2_USERNAME') || 'michiel'; + const recipientPassword = Cypress.env('NEXTCLOUD2_PASSWORD') || 'dejong'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-nc1-to-nc2.txt'; + + /** + * Test Case: Sending a federated share from one Nextcloud instance to another. + * Validates that a file can be successfully shared from Nextcloud Instance 1 to Nextcloud Instance 2. + */ + it('Send a federated share of a file from Nextcloud v28 to Nextcloud v27', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV28(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV28(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV28(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV28(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); -describe('Native federated sharing functionality for Nextcloud', () => { - it('Send federated share from Nextcloud v28 to Nextcloud v27', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); - renameFileV28('welcome.txt', 'nc1-to-nc2-share.txt') - createShareV28('nc1-to-nc2-share.txt', 'michiel', 'nextcloud2.docker') - }) + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from Nextcloud v28 to Nextcloud v27', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); - it('Receive federated share from Nextcloud v28 to Nextcloud v27', () => { - // accept share from Nextcloud 2. - cy.loginNextcloud('https://nextcloud2.docker', 'michiel', 'dejong') + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV27(); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Step 3: Navigate to the correct section + navigationSwitchLeftSideV27('Open navigation'); + selectAppFromLeftSideV27('files'); + navigationSwitchLeftSideV27('Close navigation'); - navigationSwitchLeftSideV27('Open navigation') - selectAppFromLeftSideV27('shareoverview') - navigationSwitchLeftSideV27('Close navigation') + // Step 4: Verify the shared file is visible + ensureFileExistsV27(sharedFileName); - cy.get('[data-file="nc1-to-nc2-share.txt"]', { timeout: 10000 }).should('be.visible') - }) + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-nextcloud-v28.cy.js index 434d506a..c5fa3607 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-nextcloud-v28.cy.js @@ -1,27 +1,70 @@ -import { createShareV28, renameFileV28 } from '../utils/nextcloud-v28' +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v28. + * This suite verifies the ability to send and receive federated file shares between two Nextcloud v28 instances. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ -describe('Native federated sharing functionality for Nextcloud v28', () => { - it('Send federated share from Nextcloud to Nextcloud', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') +import { + acceptShareV28, + createShareV28, + renameFileV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; - renameFileV28('welcome.txt', 'nc1-to-nc2-share.txt') - createShareV28('nc1-to-nc2-share.txt', 'michiel', 'nextcloud2.docker') - }) +describe('Native Federated Sharing Functionality for Nextcloud v28', () => { - it('Receive federated share from Nextcloud v28 to Nextcloud v28', () => { - // accept share from Nextcloud 2. - cy.loginNextcloud('https://nextcloud2.docker', 'michiel', 'dejong') + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD2_URL') || 'https://nextcloud2.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('NEXTCLOUD2_USERNAME') || 'michiel'; + const recipientPassword = Cypress.env('NEXTCLOUD2_PASSWORD') || 'dejong'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-nc1-to-nc2.txt'; - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + /** + * Test Case: Sending a federated share from one Nextcloud instance to another. + * Validates that a file can be successfully shared from Nextcloud Instance 1 to Nextcloud Instance 2. + */ + it('Send a federated share of a file from Nextcloud v28 to Nextcloud v28', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); - // force reload the page for share to apear. - cy.reload(true) + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV28(originalFileName); - cy.get('[data-cy-files-list-row-name="nc1-to-nc2-share.txt"]', { timeout: 10000 }).should('be.visible') - }) -}) + // Step 3: Rename the file to prepare it for sharing + renameFileV28(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV28(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV28(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from Nextcloud v28 to Nextcloud v28', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV28(); + + // Step 3: Reload the page to ensure the shared file appears in the file list + cy.reload(true); + + // Step 4: Verify the shared file is visible + ensureFileExistsV28(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js index 90d93123..509cb1e7 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js @@ -1,30 +1,85 @@ -import { - createShareV28, - renameFileV28 -} from '../utils/nextcloud-v28' - -describe('Native federated sharing functionality for Nextcloud v28', () => { - it('Send federated share from Nextcloud v28 to OcmStub v1.0.0', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - - renameFileV28('welcome.txt', 'nc1-to-os1-share.txt') - createShareV28('nc1-to-os1-share.txt', 'michiel', 'ocmstub1.docker') - }) - - it('Receive federated share from Nextcloud v28 to OcmStub 1.0', () => { - // accept share from OcmStub 1. - cy.loginOcmStub('https://ocmstub1.docker/?') - - cy.contains('"shareWith": "michiel@https://ocmstub1.docker"').should('be.visible') - cy.contains('"shareType": "user"').should('be.visible') - cy.contains('"name": "nc1-to-os1-share.txt"').should('be.visible') - cy.contains('"resourceType": "file"').should('be.visible') - cy.contains('"owner": "einstein@https://nextcloud1.docker/"').should('be.visible') - cy.contains('"sharedBy": "einstein@https://nextcloud1.docker/"').should('be.visible') - cy.contains('"ownerDisplayName": "einstein"').should('be.visible') - cy.contains('"description": ""').should('be.visible') - cy.contains('"protocol": { "name": "webdav", "options": { "sharedSecret": "').should('be.visible') - cy.contains('"permissions": "{http://open-cloud-mesh.org/ns}share-permissions"').should('be.visible') - }) -}) +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v28 and OcmStub v1. + * + * @author Michiel De Jong + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + createShareV28, + renameFileV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; + +import { + generateShareAssertions, +} from '../utils/ocmstub-v1.js'; + +describe('Native Federated Sharing Functionality for Nextcloud to OcmStub', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'nc1-to-os1-share.txt'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}/`, + sender: `${senderUsername}@${senderUrl}/`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + + /** + * Test Case: Sending a federated share from one Nextcloud instance to OcmStub. + * Validates that a file can be successfully shared from Nextcloud to OcmStub. + */ + it('Send a federated share of a file from Nextcloud v28 to OcmStub v1', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV28(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV28(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV28(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV28(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving a federated share on OcmStub from Nextcloud. + * + */ + it('Receive federated share of a file from from Nextcloud v28 to OcmStub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js index f5838adf..23de636d 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js @@ -1,33 +1,75 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v28 and ownCloud v10. + * This suite verifies the ability to send and receive federated file shares between Nextcloud and ownCloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { createShareV28, - renameFileV28 -} from '../utils/nextcloud-v28' + renameFileV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; import { - selectAppFromLeftSide -} from '../utils/owncloud' + acceptShare, + ensureFileExists, + selectAppFromLeftSide, +} from '../utils/owncloud'; + +describe('Native Federated Sharing Functionality for Nextcloud to ownCloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const recipientPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-nc1-to-oc1.txt'; + + /** + * Test Case: Sending a federated share from one Nextcloud instance to ownCloud. + * Validates that a file can be successfully shared from Nextcloud to ownCloud. + */ + it('Send a federated share of a file from Nextcloud v28 to ownCloud v10', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV28(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV28(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV28(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV28(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); -describe('OCM federated sharing functionality for Nextcloud', () => { - it('Send federated share from Nextcloud v28 to ownCloud v10', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); - renameFileV28('welcome.txt', 'nc1-to-oc1-share.txt') - createShareV28('nc1-to-oc1-share.txt', 'marie', 'owncloud1.docker') - }) + /** + * Test Case: Receiving and accepting a federated share on the recipient's ownCloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from Nextcloud v27 to ownCloud v10', () => { + // Step 1: Log in to the recipient's ownCloud instance + cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); - it('Receive federated share from Nextcloud v28 to ownCloud v10', () => { - // accept share from Nextcloud 2. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShare(); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Step 3: Navigate to the correct section + selectAppFromLeftSide('files'); - selectAppFromLeftSide('sharingin') + // Step 4: Verify that the shared file is visible + ensureFileExists(sharedFileName); - cy.get('[data-file="nc1-to-oc1-share.txt"]', { timeout: 10000 }).should('be.visible') - }) + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js new file mode 100644 index 00000000..a8b10f2d --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js @@ -0,0 +1,81 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v28 and OcmStub v1. + * + * @author Michiel De Jong + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + createShareV28, + renameFileV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; + +describe('Native Federated Sharing Functionality for Nextcloud to OcmStub', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'nc1-to-os1-share.txt'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}`, + sender: `${senderUsername}@${senderUrl}`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + + /** + * Test Case: Sending a federated share from one Nextcloud instance to OcmStub. + * Validates that a file can be successfully shared from Nextcloud to OcmStub. + */ + it('Send a federated share of a file from Nextcloud v28 to OcmStub v1', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV28(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV28(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV28(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV28(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving a federated share on OcmStub from Nextcloud. + * + */ + it('Receive federated share of a file from from Nextcloud v29 to OcmStub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js new file mode 100644 index 00000000..cf2ce46a --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js @@ -0,0 +1,80 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Nextcloud v28 and OcmStub v1. + * + * @author Michiel De Jong + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + createShareV28, + renameFileV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; + +describe('Native Federated Sharing Functionality for Nextcloud to OcmStub', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'nc1-to-os1-share.txt'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}`, + sender: `${senderUsername}@${senderUrl}`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + /** + * Test Case: Sending a federated share from one Nextcloud instance to OcmStub. + * Validates that a file can be successfully shared from Nextcloud to OcmStub. + */ + it('Send a federated share of a file from Nextcloud v28 to OcmStub v1', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV28(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV28(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV28(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShareV28(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving a federated share on OcmStub from Nextcloud. + * + */ + it('Receive federated share of a file from from Nextcloud v30 to OcmStub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js index f8811dcc..bf61ea7c 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js @@ -1,20 +1,67 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in OcmStub. + * + * @author Michiel De Jong + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + generateShareAssertions, +} from '../utils/ocmstub-v1.js'; + describe('Native federated sharing functionality for OcmStub', () => { - it('Send federated share from OcmStub 1.0 to OcmStub 1.0', () => { - cy.visit('https://ocmstub1.docker/shareWith?marie@ocmstub2.docker') - cy.contains('yes shareWith').should('be.visible') - }) - - it('Receive federated share from OcmStub v1.0.0 to OcmStub v1.0.0', () => { - // accept share from OcmStub 2. - cy.loginOcmStub('https://ocmstub2.docker/?') - - cy.contains('"shareWith": "marie@ocmstub2.docker"').should('be.visible') - cy.contains('"shareType": "user"').should('be.visible') - cy.contains('"name": "Test share from stub"').should('be.visible') - cy.contains('"resourceType": "file"').should('be.visible') - cy.contains('"owner": "einstein@ocmstub1.docker"').should('be.visible') - cy.contains('"sender": "einstein@ocmstub1.docker"').should('be.visible') - cy.contains('"ownerDisplayName": "einstein"').should('be.visible') - cy.contains('"protocol": { "name": "webdav", "options": { "sharedSecret": "').should('be.visible') - }) + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const recipientUrl = Cypress.env('OCMSTUB2_URL') || 'https://ocmstub2.docker'; + const senderUsername = Cypress.env('OCMSTUB1_USERNAME') || 'einstein'; + const recipientUsername = Cypress.env('OCMSTUB2_USERNAME') || 'mahdi'; + const expectedMessage = 'yes shareWith'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + fileName: 'Test share from stub', + owner: `${senderUsername}@${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`, + sender: `${senderUsername}@${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + + /** + * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + */ + it('should successfully send a federated share of a file from OcmStub 1.0 to OcmStub 1.0', () => { + // Step 1: Navigate to the federated share link on OcmStub 1.0 + // Remove trailing slash and leading https or http from recipientUrl + cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); + + // Step 2: Verify the confirmation message is displayed + cy.contains(expectedMessage, { timeout: 10000 }) + .should('be.visible') + }); + + /** + * Test Case: Receiving a federated share on OcmStub from ocmStub. + * + */ + it('Receive federated share of a file from from OcmStub v1 to OcmStub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js index 0591df4c..b3554947 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js @@ -1,36 +1,78 @@ -import { - createShare, - renameFile -} from '../utils/owncloud' +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in ownCloud v10 and Nextcloud v28. + * This suite verifies the ability to send and receive federated file shares between ownCloud and Nextcloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ import { + acceptShareV27, + ensureFileExistsV27, navigationSwitchLeftSideV27, selectAppFromLeftSideV27, -} from '../utils/nextcloud-v27' +} from '../utils/nextcloud-v27'; + +import { + createShare, + renameFile, + ensureFileExists, +} from '../utils/owncloud'; describe('OCM federated sharing functionality for ownCloud', () => { - it('Send federated share from ownCloud v10 to Nextcloudn v27', () => { - // share from ownCloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - - renameFile('welcome.txt', 'oc1-to-nc1-share.txt') - createShare('oc1-to-nc1-share.txt', 'einstein', 'nextcloud1.docker') - }) - - it('Receive federated share from ownCloud v10 to Nextcloudn v27', () => { - // accept share from Nextcloud 2. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() - - navigationSwitchLeftSideV27('Open navigation') - selectAppFromLeftSideV27('shareoverview') - navigationSwitchLeftSideV27('Close navigation') - - cy.get('[data-file="oc1-to-nc1-share.txt"]', { timeout: 10000 }).should('be.visible') - }) + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const senderUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const senderPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-oc1-to-nc1.txt'; + + /** + * Test Case: Sending a federated share from one ownCloud to Nextcloud. + * Validates that a file can be successfully shared from ownCloud to Nextcloud. + */ + it('Send a federated share of a file from ownCloud v10 to Nextcloudn v27', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists + ensureFileExists(originalFileName); + + // Step 3: Rename the file + renameFile(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExists(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShare(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from ownCloud v10 to Nextcloud v27', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV27(); + + // Step 3: Navigate to the correct section + navigationSwitchLeftSideV27('Open navigation'); + selectAppFromLeftSideV27('files'); + navigationSwitchLeftSideV27('Close navigation'); + + // Step 4: Verify the shared file is visible + ensureFileExistsV27(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js index 9048765f..907bab47 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js @@ -1,27 +1,74 @@ -import { createShare, renameFile } from '../utils/owncloud' +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in ownCloud v10 and Nextcloud v28. + * This suite verifies the ability to send and receive federated file shares between ownCloud and Nextcloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + acceptShareV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; + +import { + createShare, + renameFile, + ensureFileExists, +} from '../utils/owncloud'; describe('OCM federated sharing functionality for ownCloud', () => { - it('Send federated share from ownCloud v10 to Nextcloudn v28', () => { - // share from ownCloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - renameFile('welcome.txt', 'oc1-to-nc1-share.txt') - createShare('oc1-to-nc1-share.txt', 'einstein', 'nextcloud1.docker') - }) + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const senderUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const senderPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-oc1-to-nc1.txt'; + + /** + * Test Case: Sending a federated share from one ownCloud to Nextcloud. + * Validates that a file can be successfully shared from ownCloud to Nextcloud. + */ + it('Send a federated share of a file from ownCloud v10 to Nextcloudn v28', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists + ensureFileExists(originalFileName); + + // Step 3: Rename the file + renameFile(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExists(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShare(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from ownCloud v10 to Nextcloud v28', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); - it('Receive federated share from ownCloud v10 to Nextcloudn v28', () => { - // accept share from Nextcloud 2. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV28(); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Step 3: Reload the page to ensure the shared file appears in the file list + cy.reload(true); - // force reload the page for share to apear. - cy.reload(true) + // Step 4: Verify the shared file is visible + ensureFileExistsV28(sharedFileName); - cy.get('[data-cy-files-list-row-name="oc1-to-nc1-share.txt"]', { timeout: 10000 }).should('be.visible') - }) + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js index 6eb07cdc..958e895b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js @@ -1,28 +1,85 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in ownCloud v10 and OcmStub v1. + * + * @author Michiel De Jong + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { createShare, renameFile, -} from '../utils/owncloud' - -describe('Native federated sharing functionality for ownCloud', () => { - it('Send federated share from OcmStub v1.0.0 to ownCloud v10', () => { - // share from ownCloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - - renameFile('welcome.txt', 'oc1-to-os1-share.txt') - createShare('oc1-to-os1-share.txt', 'michiel', 'ocmstub1.docker') - }) - - it('Receive federated share from ownCloud v10 to OcmStub v1.0.0', () => { - // accept share from OcmStub 1. - cy.loginOcmStub('https://ocmstub1.docker/?') - - cy.contains('"shareWith": "michiel@ocmstub1.docker"').should('be.visible') - cy.contains('"shareType": "user"').should('be.visible') - cy.contains('"name": "oc1-to-os1-share.txt"').should('be.visible') - cy.contains('"resourceType": "file"').should('be.visible') - cy.contains('"owner": "marie@https://owncloud1.docker"').should('be.visible') - cy.contains('"sender": "marie@https://owncloud1.docker"').should('be.visible') - cy.contains('"ownerDisplayName": "marie"').should('be.visible') - cy.contains('"protocol": { "name": "webdav", "options": { "sharedSecret": "').should('be.visible') - }) + ensureFileExists, +} from '../utils/owncloud'; + +import { + generateShareAssertions, +} from '../utils/ocmstub-v1.js'; + +describe('Native Federated Sharing Functionality for ownCloud to OcmStub', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const senderUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const senderPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'oc1-to-os1-share.txt'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}`, + sender: `${senderUsername}@${senderUrl}`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + + /** + * Test Case: Sending a federated share from one ownCloud to OcmStub. + * Validates that a file can be successfully shared from ownCloud to OcmStub. + */ + it('Send a federated share of a file from ownCloud v10 to OcmStub v1', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists + ensureFileExists(originalFileName); + + // Step 3: Rename the file + renameFile(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExists(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShare(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving a federated share on OcmStub from ownCloud. + * + */ + it('Receive federated share of a file from from ownCloud v10 to OcmStub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js index e5c80ac4..48c6d663 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js @@ -1,30 +1,71 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in ownCloud v10. + * This suite verifies the ability to send and receive federated file shares between two ownCloud instances. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { + acceptShare, createShare, renameFile, - selectAppFromLeftSide -} from '../utils/owncloud' + ensureFileExists, + selectAppFromLeftSide, +} from '../utils/owncloud'; describe('Native federated sharing functionality for ownCloud', () => { - it('Send federated share from ownCloud v10 to ownCloud v10', () => { - // share from ownCloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - renameFile('welcome.txt', 'oc1-to-oc2-share.txt') - createShare('oc1-to-oc2-share.txt', 'mahdi', 'owncloud2.docker') - }) + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUrl = Cypress.env('OWNCLOUD2_URL') || 'https://owncloud2.docker'; + const senderUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const senderPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const recipientUsername = Cypress.env('OWNCLOUD2_USERNAME') || 'mahdi'; + const recipientPassword = Cypress.env('OWNCLOUD2_PASSWORD') || 'baghbani'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-oc1-to-oc2.txt'; + + /** + * Test Case: Sending a federated share from one ownCloud instance to another. + * Validates that a file can be successfully shared from ownCloud Instance 1 to ownCloud Instance 2. + */ + it('Send a federated share of a file from ownCloud v10 to ownCloud v10', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists + ensureFileExists(originalFileName); + + // Step 3: Rename the file + renameFile(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExists(sharedFileName); + + // Step 5: Create a federated share for the recipient + createShare(sharedFileName, recipientUsername, recipientUrl.replace(/^https?:\/\/|\/$/g, '')); + + // TODO @MahdiBaghbani: Verify that the share was created successfully + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's ownCloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from ownCloud v10 to ownCloud v10', () => { + // Step 1: Log in to the recipient's ownCloud instance + cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); - it('Receive federated share from ownCloud v10 to ownCloud v10', () => { - // accept share from Nextcloud 2. - cy.loginOwncloud('https://owncloud2.docker', 'mahdi', 'baghbani') + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShare(); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Step 3: Navigate to the correct section + selectAppFromLeftSide('files'); - selectAppFromLeftSide('sharingin') + // Step 4: Verify that the shared file is visible + ensureFileExists(sharedFileName); - cy.get('[data-file="oc1-to-oc2-share.txt"]', { timeout: 10000 }).should('be.visible') - }) + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/general.js b/cypress/ocm-test-suite/cypress/e2e/utils/general.js new file mode 100644 index 00000000..b5f11e5e --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/utils/general.js @@ -0,0 +1,18 @@ +/** + * @fileoverview + * Utility functions for Cypress tests. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +/** + * Escapes special characters in a string to be used in a CSS selector. + * This is necessary because file names may contain characters that have special meanings in CSS selectors. + * + * @param {string} selector - The string to escape. + * @returns {string} - The escaped string safe for use in a CSS selector. + */ +export function escapeCssSelector(selector) { + // Replace any character that is a CSS selector special character with its escaped version + return selector.replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, '\\$1'); +} diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index e566c139..809151db 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -1,207 +1,431 @@ +/** + * @fileoverview + * Utility functions for Cypress tests interacting with Nextcloud version 27. + * These functions provide abstractions for common actions such as sharing files, + * updating permissions, renaming files, and navigating the UI. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + escapeCssSelector, +} from './general'; + +/** + * Ensures that a file with the specified name exists and is visible in the file list. + * + * This function waits for the file element to appear in the DOM within the specified timeout and checks that it is visible. + * If the file does not exist or is not visible within the timeout, the test will fail with an appropriate error message. + * + * @param {string} fileName - The name of the file to check. + * @param {number} [timeout=10000] - Optional timeout in milliseconds for the check. Defaults to 10000ms. + * + * @example + * // Ensure that the file 'example.txt' exists and is visible + * ensureFileExists('example.txt'); + * + * @throws Will cause the test to fail if the file does not exist or is not visible within the timeout. + */ +export function ensureFileExistsV27(fileName, timeout = 10000) { + // Escape special characters in the file name to safely use it in a CSS selector + const escapedFileName = escapeCssSelector(fileName); + + // Wait for the file row to exist in the DOM and be visible + cy.get(`[data-file="${escapedFileName}"]`, { timeout }) + .should('exist') + .and('be.visible'); +} + +/** + * Accepts a share dialog by clicking the "primary" button. + */ export function acceptShareV27() { - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Wait for the share dialog to appear and ensure it's visible + cy.get('div.oc-dialog', { timeout: 10000 }) + .should('be.visible') + .within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') + .find('button.primary') + .should('be.visible') + .click(); + }); } +/** + * Creates a share for a specific file and user. + * @param {string} fileName - The name of the file to be shared. + * @param {string} username - The username of the recipient. + * @param {string} domain - The domain of the recipient. + */ export function createShareV27(fileName, username, domain) { - openSharingPanelV27(fileName) - - cy.get('#app-sidebar-vue').within(() => { - cy.get('#sharing-search-input').clear() - cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch') - cy.get('#sharing-search-input').type(username + '@' + domain) - cy.wait('@userSearch') - }) - - // ensure selecting remote [sharetype="6"] instead of email! - cy.get(`[user="${username}"]`).should('be.visible').click() - cy.get('div[class="button-group"]').contains('Save share').should('be.visible').click() + // Open the sharing panel for the specified file + openSharingPanelV27(fileName); + + // Set up an intercept for the user search API request + cy.intercept('GET', '**/apps/files_sharing/api/v1/sharees?*').as('userSearch'); + + cy.get('#app-sidebar-vue').within(() => { + // Clear the search input and type the recipient's email + cy.get('#sharing-search-input') + .clear() + .type(`${username}@${domain}`); + }); + + // Wait for the user search API request to complete + cy.wait('@userSearch'); + + // Select the correct user from the search results + cy.get(`[user="${username}"]`) + .should('be.visible') + .click(); + + // Click the "Save share" button to finalize the share + cy.get('div.button-group') + .contains('Save share') + .should('be.visible') + .click(); } +/** + * Creates a shareable link for a file and returns the copied link. + * @param {string} fileName - The name of the file to create a link for. + * @returns {Cypress.Chainable} - A chainable containing the copied share link. + */ export function createShareLinkV27(fileName) { - openSharingPanelV27(fileName) - - return cy.window().then(win => { - cy.stub(win.navigator.clipboard, 'writeText').as('copy'); - - cy.get('#app-sidebar-vue').within(() => { - cy.get('button[title="Create a new share link"]') - .should('be.visible') - .click() - }) - - return cy.get('@copy').should('have.been.calledOnce').then((spy) => { - return (spy).lastCall.args[0]; - }); - }) + // Open the sharing panel for the specified file + openSharingPanelV27(fileName); + + // Stub the clipboard API to intercept the copied link + cy.window().then((win) => { + cy.stub(win.navigator.clipboard, 'writeText').as('copy'); + }); + + cy.get('#app-sidebar-vue').within(() => { + // Locate and click the "Create a new share link" button + cy.get('button[title="Create a new share link"]') + .should('be.visible') + .click(); + }); + + // Verify that the link was copied to the clipboard and retrieve it + return cy.get('@copy').should('have.been.calledOnce').then((stub) => { + const copiedLink = stub.args[0][0]; + return copiedLink; + }); } -export function createInviteTokenV27(senderDomain) { - - cy.get('button[id="token-generator"]').should('be.visible').click() - - return cy.get('input[class="generated-token-link"]') - .invoke('val') - .then( - sometext => { - // extract token from url. - const token = sometext.replace('https://meshdir.docker/meshdir?token=', ''); - - return token.replace(`&providerDomain=${senderDomain}`, '') - } - ); +/** + * Generates an invite token for federated sharing. + * Extracts the token from the input field and returns it. + * @returns {Cypress.Chainable} - A chainable containing the extracted invite token. + */ +export function createInviteToken() { + // Ensure the "Generate Token" button is visible and click it + cy.get('button#token-generator') + .should('be.visible') + .click(); + + // Extract and process the token from the input field + return cy.get('input.generated-token-link') + .invoke('val') + .then((link) => { + if (!link) { + throw new Error('Token generation failed: No token found in the input field.'); + } + // Use URLSearchParams to parse the link and extract the token + const url = new URL(link); + const token = url.searchParams.get('token'); + if (!token) { + throw new Error('Token generation failed: Token parameter not found in the URL.'); + } + return token; + }); } +/** + * Generates an invite link for federated sharing. + * Combines the extracted token with the target domain to create an invite link. + * @param {string} targetDomain - The domain of the recipient. + * @returns {Cypress.Chainable} - A chainable containing the generated invite link. + */ export function createInviteLinkV27(targetDomain) { - - cy.get('button[id="token-generator"]').should('be.visible').click() - - return cy.get('input[class="generated-token-link"]') - .invoke('val') - .then( - sometext => { - // extract token from url. - const token = sometext.replace('https://meshdir.docker/meshdir?', ''); - - // put target efss domain and token together. - const inviteLink = `${targetDomain}/index.php/apps/sciencemesh/accept?${token}` - - return inviteLink - } - ); + // Ensure the "Generate Token" button is visible and click it + cy.get('button#token-generator') + .should('be.visible') + .click(); + + // Extract the token and construct the invite link + return cy.get('input.generated-token-link') + .invoke('val') + .then((link) => { + if (!link) { + throw new Error('Invite link generation failed: No link found in the input field.'); + } + // Extract the query parameters from the link + const url = new URL(link); + const queryParams = url.searchParams.toString(); + // Construct the invite link with the target domain + return `${targetDomain}/index.php/apps/sciencemesh/accept?${queryParams}`; + }); } +/** + * Verifies a federated contact in the contacts table. + * @param {string} domain - The domain of the application. + * @param {string} displayName - The display name of the contact. + * @param {string} contactDomain - The expected domain of the contact. + */ export function verifyFederatedContactV27(domain, displayName, contactDomain) { - cy.visit(`https://${domain}/index.php/apps/sciencemesh/contacts`) - - cy.get('table[id="contact-table"]') - .find('p[class="displayname"]') - .should("have.text", displayName) - - cy.get('table[id="contact-table"]') - .find('p[class="displayname"]') - .contains(displayName) - .parent() - .parent() - .find('p[class="username-provider"]') - .invoke('text') - .then( - usernameWithDomain => { - const extractedDomain = usernameWithDomain.substring( - usernameWithDomain.lastIndexOf("@") + 1, usernameWithDomain.length - ) - - expect(extractedDomain).equal(contactDomain); - } - ) + cy.visit(`https://${domain}/index.php/apps/sciencemesh/contacts`); + + cy.get('table#contact-table') + .find('p.displayname', { timeout: 10000 }) + .contains(displayName) // Ensure the display name is present + .closest('tr') // Traverse to the parent row + .find('p.username-provider') + .invoke('text') // Extract the username and domain text + .then((usernameWithDomain) => { + const extractedDomain = usernameWithDomain.split('@').pop(); // Extract domain after '@' + expect(extractedDomain).to.equal(contactDomain); // Assert the domain matches + }); +} + +/** + * Accepts an invitation by clicking the accept button in the invitation dialog. + * + * This function waits for the invitation accept button to be visible and enabled within the specified timeout, + * and then clicks it to accept the invitation. + * + * @param {number} [timeout=10000] - Optional timeout in milliseconds for waiting for the accept button. Defaults to 10000ms. + * + * @example + * // Accept the invitation + * acceptScienceMeshInvitation(5000); + * + * @throws Will cause the test to fail if the accept button is not visible or interactable within the timeout. + */ +export function acceptScienceMeshInvitation(timeout = 10000) { + // Wait for the accept button to be visible and enabled + cy.get('input#accept-button', { timeout }) + .should('be.visible') + // Ensure the button is not disabled + .and('not.be.disabled') + .click(); } +/** + * Retrieves the ScienceMesh contact ID from a display name and verifies the contact. + * @param {string} domain - The domain of the ScienceMesh instance. + * @param {string} displayName - The display name of the contact. + * @param {string} contactDomain - The expected domain of the contact. + * @returns {Cypress.Chainable} - A chainable containing the contact ID in the format "username@domain". + */ export function getScienceMeshContactIdFromDisplayNameV27(domain, displayName, contactDomain) { - verifyFederatedContactV27(domain, displayName, contactDomain) - - return cy.get('table[id="contact-table"]') - .find('p[class="displayname"]') - .contains(displayName) - .parent() - .parent() - .find('p[class="username-provider"]') - .invoke('text') - .then( - usernameWithDomain => { - // get the index of the last @ - var lastIndex = usernameWithDomain.lastIndexOf('@'); - var username = usernameWithDomain.substr(0, lastIndex) - var domain = usernameWithDomain.substr(lastIndex + 1) - - // remove https:// or http:// from domain (reva likes to show username@https://domain sometimes) - // I'm too afraid to explain the regex here let's just say its from here: https://stackoverflow.com/a/8206299/8549230 - domain = domain.replace(/^\/\/|^.*?:(\/\/)?/, ''); - - return username + '@' + domain - } - ) -} - -export function createScienceMeshShareV27(domain, displayName, contactDomain, filename) { - getScienceMeshContactIdFromDisplayNameV27(domain, displayName, contactDomain).then( - (shareWith) => { - cy.visit(`https://${domain}/index.php/apps/files`) - - openSharingPanelV27(filename) - - cy.get('#app-sidebar-vue').within(() => { - cy.get('#sharing-search-input').clear() - cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch') - cy.get('#sharing-search-input').type(displayName) - cy.wait('@userSearch') - }) - - cy.get(`[sharewith="${shareWith}"]`) - .eq(0) - .should('be.visible') - .click() - - cy.get('div[class="button-group"]') - .contains('Save share') - .should('be.visible') - .click() - } - ) + // Verify that the contact exists in the table + verifyFederatedContactV27(domain, displayName, contactDomain); + + // Locate the contact in the table and extract the username and domain + return cy.get('table#contact-table') + .find('p.displayname') + .contains(displayName) // Ensure the correct display name is found + .closest('tr') // Traverse to the parent row + .find('p.username-provider') + .invoke('text') // Extract the full username and domain text + .then((usernameWithDomain) => { + // Extract username and domain + const lastIndex = usernameWithDomain.lastIndexOf('@'); + const username = usernameWithDomain.substring(0, lastIndex); // Extract username + let extractedDomain = usernameWithDomain.substring(lastIndex + 1); // Extract domain + + // Remove protocols (e.g., https:// or http://) from the domain + extractedDomain = extractedDomain.replace(/^https?:\/\/|^\/\/|www\./, ''); + + // Return the contact ID in the format "username@domain" + return `${username}@${extractedDomain}`; + }); } +/** + * Creates a ScienceMesh share for a specific contact and file. + * @param {string} domain - The domain of the ScienceMesh instance. + * @param {string} displayName - The display name of the contact. + * @param {string} contactDomain - The domain of the contact. + * @param {string} fileName - The name of the file to be shared. + */ +export function createScienceMeshShareV27(domain, displayName, contactDomain, fileName) { + // Retrieve the contact ID + getScienceMeshContactIdFromDisplayNameV27(domain, displayName, contactDomain).then((shareWith) => { + // Navigate to the files app + cy.visit(`https://${domain}/index.php/apps/files`); + + // Open the sharing panel for the file + openSharingPanelV27(fileName); + + // Set up an intercept for the user search API request + cy.intercept('GET', '**/apps/files_sharing/api/v1/sharees?*').as('userSearch'); + + cy.get('#app-sidebar-vue').within(() => { + // Clear the search input and type the contact's display name + cy.get('#sharing-search-input') + .clear() + .type(displayName); + }); + + // Wait for the user search API request to complete + cy.wait('@userSearch'); + + // Select the contact from the search results + cy.get(`[sharewith="${shareWith}"]`) + .eq(0) // Ensure the correct match is selected + .should('be.visible') // Assert visibility + .click(); // Click to select the contact + + // Click the "Save share" button to finalize the share + cy.get('div.button-group') + .contains('Save share') + .should('be.visible') + .click(); + }); +} + +/** + * Renames a file and waits for the move operation to complete. + * @param {string} fileName - The current name of the file. + * @param {string} newFileName - The new name for the file. + */ export function renameFileV27(fileName, newFileName) { - triggerActionInFileMenuV27(fileName, 'Rename') + // Trigger the "Rename" action from the file's menu + triggerActionInFileMenuV27(fileName, 'Rename'); + + // Intercept the MOVE API request for renaming files + cy.intercept('MOVE', /\/remote\.php\/dav\/files\//).as('moveFile'); + + // Find the file row and enter the new file name + const fileRow = getRowForFileV27(fileName); + fileRow.find('form input') + .clear() + .type(`${newFileName}{enter}`); - // intercept the move so we can wait for it. - cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile') - getRowForFileV27(fileName).find('form').find('input').clear() - getRowForFileV27(fileName).find('form').find('input').type(`${newFileName}{enter}`) - cy.wait('@moveFile') + // Wait for the move operation to complete + cy.wait('@moveFile'); } +/** + * Opens the sharing panel for a specific file. + * @param {string} fileName - The name of the file. + */ export function openSharingPanelV27(fileName) { - triggerActionForFileV27(fileName, 'Share') - - cy.get('#app-sidebar-vue') - .get('[aria-controls="tab-sharing"]') - .should('be.visible') - .click() + triggerActionForFileV27(fileName, 'Share'); + + // Ensure the sharing tab is visible and click it + cy.get('#app-sidebar-vue').within(() => { + cy.get('[aria-controls="tab-sharing"]') + .should('be.visible') + .click(); + }); } -// actionId possible values are: -// 1. Open navigation -// 2. Close navigation +/** + * Toggles the left-side navigation panel based on the provided action. + * @param {string} actionId - The action to perform (e.g., "Open navigation", "Close navigation"). + * Valid values: + * - "Open navigation" + * - "Close navigation" + */ export function navigationSwitchLeftSideV27(actionId) { - cy.get('div[id="app-navigation-vue"]') - .find(`button[aria-label="${CSS.escape(actionId)}"]`) - .should('be.visible') - .click() + const validActions = ["Open navigation", "Close navigation"]; + + // Validate the actionId + if (!validActions.includes(actionId)) { + throw new Error(`Invalid actionId: "${actionId}". Valid options are ${validActions.join(", ")}.`); + } + + // Find the button for the specified action and click it + cy.get('div#app-navigation-vue', { timeout: 10000 }) + .find(`button[aria-label="${actionId}"]`) + .should('be.visible') + .click(); } -// appId possible values are: -// 1. files -// 2. recent -// 3. favorites -// 4. shareoverview -// 5. systemtagsfilter -// 6. trashbin +/** + * Selects an app from the left-side navigation menu. + * @param {string} appId - The identifier of the app to select. + * Valid values: + * - "files" + * - "recent" + * - "favorites" + * - "shareoverview" + * - "systemtagsfilter" + * - "trashbin" + */ export function selectAppFromLeftSideV27(appId) { - cy.get('div[id="app-navigation-vue"]') - .find(`li[data-cy-files-navigation-item="${CSS.escape(appId)}"]`) - .should('be.visible') - .click() + const validAppIds = [ + "files", + "recent", + "favorites", + "shareoverview", + "systemtagsfilter", + "trashbin" + ]; + + // Validate the appId + if (!validAppIds.includes(appId)) { + throw new Error(`Invalid appId: "${appId}". Valid options are ${validAppIds.join(", ")}.`); + } + + // Find the app in the navigation menu and click it + cy.get('div#app-navigation-vue', { timeout: 10000 }) + .find(`li[data-cy-files-navigation-item="${appId}"]`) + .should('be.visible') + .click(); } +/** + * Triggers an action (e.g., rename, details) in a file's menu. + * @param {string} filename - The name of the file. + * @param {string} actionId - The action to trigger. + */ export function triggerActionInFileMenuV27(fileName, actionId) { - triggerActionForFileV27(fileName, 'menu') - getRowForFileV27(fileName).find('*[class^="filename"]').find('*[class^="fileActionsMenu"]').find(`[data-action="${CSS.escape(actionId)}"]`).should('be.visible').click() + // Open the file's action menu + triggerActionForFileV27(fileName, 'menu'); + + // Find the specific action within the menu and click it + getRowForFileV27(fileName) + .find(`*[data-action="${actionId}"]`) + .should('be.visible') + .as('btn') + .click(); } -export const triggerActionForFileV27 = (filename, actionId) => getActionsForFileV27(filename).find(`[data-action="${CSS.escape(actionId)}"]`).should('be.visible').click() +/** + * Triggers an action for a specific file. + * @param {string} fileName - The name of the file. + * @param {string} actionId - The action to trigger. + */ +export function triggerActionForFileV27(fileName, actionId) { + // Find the actions container for the file + getActionsForFileV27(fileName) + .find(`*[data-action="${actionId}"]`) + .should('be.visible') + .as('btn') + .click(); +} -export const getActionsForFileV27 = (filename) => getRowForFileV27(filename).find('*[class^="filename"]').find('*[class^="name"]').find('*[class^="fileactions"]') +/** + * Retrieves the actions container for a specific file. + * @param {string} fileName - The name of the file. + * @returns {Cypress.Chainable>} - The actions container element. + */ +export function getActionsForFileV27(fileName) { + return getRowForFileV27(fileName).find('.fileactions'); +} -export const getRowForFileV27 = (filename) => cy.get(`[data-file="${CSS.escape(filename)}"]`) +/** + * Retrieves the row element for a specific file. + * @param {string} fileName - The name of the file. + * @returns {Cypress.Chainable>} - The file row element. + */ +export function getRowForFileV27(fileName) { + return cy.get(`[data-file="${fileName}"]`); +} diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js index 652acfe6..57a53d43 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js @@ -1,111 +1,241 @@ +/** + * @fileoverview + * Utility functions for Cypress tests interacting with Nextcloud version 28. + * These functions provide abstractions for common actions such as sharing files, + * updating permissions, renaming files, and navigating the UI. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + escapeCssSelector, +} from './general'; + +/** + * Ensures that a file with the specified name exists and is visible in the file list. + * + * This function waits for the file element to appear in the DOM within the specified timeout and checks that it is visible. + * If the file does not exist or is not visible within the timeout, the test will fail with an appropriate error message. + * + * @param {string} fileName - The name of the file to check. + * @param {number} [timeout=10000] - Optional timeout in milliseconds for the check. Defaults to 10000ms. + * + * @example + * // Ensure that the file 'example.txt' exists and is visible + * ensureFileExists('example.txt'); + * + * @throws Will cause the test to fail if the file does not exist or is not visible within the timeout. + */ +export function ensureFileExistsV28(fileName, timeout = 10000) { + // Escape special characters in the file name to safely use it in a CSS selector + const escapedFileName = escapeCssSelector(fileName); + + // Wait for the file row to exist in the DOM and be visible + cy.get(`[data-cy-files-list-row-name="${escapedFileName}"]`, { timeout }) + .should('exist') + .and('be.visible'); +} + +/** + * Accepts a share dialog by clicking the "primary" button. + */ export function acceptShareV28() { - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .should('be.visible') - .click() + // Wait for the share dialog to appear and ensure it's visible + cy.get('div.oc-dialog', { timeout: 10000 }) + .should('be.visible') + .within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') + .find('button.primary') + .should('be.visible') + .click(); + }); } +/** + * Creates a share for a specific file and user. + * @param {string} fileName - The name of the file to be shared. + * @param {string} username - The username of the recipient. + * @param {string} domain - The domain of the recipient. + */ export function createShareV28(fileName, username, domain) { - openSharingPanelV28(fileName) - - cy.get('#app-sidebar-vue').within(() => { - cy.get('#sharing-search-input').clear() - cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch') - cy.get('#sharing-search-input').type(username + '@' + domain) - cy.wait('@userSearch') - }) - - // ensure selecting remote [sharetype="6"] instead of email! - cy.get(`[user="${username}"]`).should('be.visible').click() - - cy.get('*[class^="sharingTabDetailsView"]') - .find('*[class^="sharingTabDetailsView__footer"]') - .find('*[class^="button-group"]') - .find('[data-cy-files-sharing-share-editor-action="save"]') - .should('be.visible') - .click({ scrollBehavior: 'nearest' }) - - // HACK: Save the share and then update it, as permissions changes are currently not saved for new share. - // updateShareV28(fileName, 0) // @MahdiBaghbani: not sure about this yet. + // Open the sharing panel for the specified file + openSharingPanelV28(fileName); + + // Set up an intercept for the user search API request + cy.intercept('GET', '**/apps/files_sharing/api/v1/sharees?*').as('userSearch'); + + cy.get('#app-sidebar-vue').within(() => { + // Clear the input field and type the recipient's email + cy.get('#sharing-search-input') + .clear() + .type(`${username}@${domain}`); + }); + + // Wait for the user search API request to complete + cy.wait('@userSearch'); + + // Select the correct user from the search results + cy.get(`[user="${username}"]`) + .should('be.visible') + .click(); + + cy.get('#app-sidebar-vue').within(() => { + // Click the "Save share" button to finalize the share + cy.get('div.sharingTabDetailsView__footer button[data-cy-files-sharing-share-editor-action="save"]') + .should('be.visible') + .click({ scrollBehavior: 'nearest' }); + }); } +/** + * Creates a shareable link for a file and returns the copied link. + * @param {string} fileName - The name of the file to create a link for. + * @returns {Cypress.Chainable} - A chainable containing the copied share link. + */ export function createShareLinkV28(fileName) { - openSharingPanelV28(fileName) - - return cy.window().then(win => { - cy.stub(win.navigator.clipboard, 'writeText').as('copy'); - - cy.get('#app-sidebar-vue').within(() => { - cy.get('*[id^="tab-sharing"]') - .find('*[title^="Create a new share link"]') - .should('be.visible') - .click() - }) - - return cy.get('@copy').should('have.been.calledOnce').then((spy) => { - return (spy).lastCall.args[0]; - }); - }) + // Open the sharing panel for the specified file + openSharingPanelV28(fileName); + + // Stub the clipboard API to intercept the copied link + cy.window().then((win) => { + cy.stub(win.navigator.clipboard, 'writeText').as('copy'); + }); + + cy.get('#app-sidebar-vue').within(() => { + // Locate and click the "Create a new share link" button + cy.get('button[title="Create a new share link"]') + .should('be.visible') + .click(); + }); + + // Verify that the link was copied to the clipboard and retrieve it + return cy.get('@copy').should('have.been.calledOnce').then((stub) => { + const copiedLink = stub.args[0][0]; + return copiedLink; + }); } +/** + * Updates sharing permissions for a specific share. + * @param {string} fileName - The name of the shared file. + * @param {number} index - The index of the share to update (0-based). + */ export function updateShareV28(fileName, index) { - openSharingPanelV28(fileName) - - cy.get('#app-sidebar-vue').within(() => { - cy.get('[data-cy-files-sharing-share-actions]').eq(index).click() - cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]').click() - - cy.get('[data-cy-files-sharing-share-permissions-checkbox="download"]').find('input').as('downloadCheckbox') - // Force:true because the checkbox is hidden by the pretty UI. - cy.get('@downloadCheckbox').check({ force: true, scrollBehavior: 'nearest' }) - - cy.get('[data-cy-files-sharing-share-permissions-checkbox="read"]').find('input').as('readCheckbox') - // Force:true because the checkbox is hidden by the pretty UI. - cy.get('@readCheckbox').check({ force: true, scrollBehavior: 'nearest' }) - - cy.get('[data-cy-files-sharing-share-permissions-checkbox="update"]').find('input').as('updateCheckbox') - // Force:true because the checkbox is hidden by the pretty UI. - cy.get('@updateCheckbox').check({ force: true, scrollBehavior: 'nearest' }) - - cy.get('[data-cy-files-sharing-share-permissions-checkbox="delete"]').find('input').as('deleteCheckbox') - // Force:true because the checkbox is hidden by the pretty UI. - cy.get('@deleteCheckbox').check({ force: true, scrollBehavior: 'nearest' }) - - cy.get('[data-cy-files-sharing-share-editor-action="save"]').click({ scrollBehavior: 'nearest' }) - }) + // Open the sharing panel for the specified file + openSharingPanelV28(fileName); + + cy.get('#app-sidebar-vue').within(() => { + // Open the actions menu for the specified share + cy.get('[data-cy-files-sharing-share-actions]') + .eq(index) + .should('be.visible') + .click(); + + // Select custom permissions + cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]') + .should('be.visible') + .click(); + + // Update each permission checkbox + ['download', 'read', 'update', 'delete'].forEach((permission) => { + cy.get(`[data-cy-files-sharing-share-permissions-checkbox="${permission}"] input`) + .check({ force: true, scrollBehavior: 'nearest' }); + }); + + // Save the changes + cy.get('button[data-cy-files-sharing-share-editor-action="save"]') + .should('be.visible') + .click({ scrollBehavior: 'nearest' }); + }); } -export const renameFileV28 = (fileName, newFileName) => { - getRowForFileV28(fileName) - triggerActionForFileV28(fileName, 'rename') - - // intercept the move so we can wait for it. - cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile') - getRowForFileV28(fileName).find('[data-cy-files-list-row-name] input').clear() - getRowForFileV28(fileName).find('[data-cy-files-list-row-name] input').type(`${newFileName}{enter}`) - cy.wait('@moveFile') +/** + * Renames a file and waits for the move operation to complete. + * @param {string} fileName - The current name of the file. + * @param {string} newFileName - The new name for the file. + */ +export function renameFileV28(fileName, newFileName) { + // Trigger the "Rename" action from the file's menu + triggerActionForFileV28(fileName, 'rename'); + + // Intercept the MOVE API request for renaming files + cy.intercept('MOVE', '**/remote.php/dav/files/**').as('moveFile'); + + // Find the file row and enter the new file name + const fileRow = getRowForFileV28(fileName); + fileRow.find('[data-cy-files-list-row-name] input') + .should('be.visible') + .clear() + .type(`${newFileName}{enter}`); + + // Wait for the move operation to complete + cy.wait('@moveFile'); } +/** + * Opens the sharing panel for a specific file. + * @param {string} fileName - The name of the file. + */ export function openSharingPanelV28(fileName) { - triggerActionForFileV28(fileName, 'details') - - cy.get('#app-sidebar-vue') - .get('[aria-controls="tab-sharing"]') - .should('be.visible') - .click() + // Trigger the "Details" action to open the sidebar + triggerActionForFileV28(fileName, 'details'); + + // Ensure the sharing tab is visible and click it + cy.get('#app-sidebar-vue').within(() => { + cy.get('[aria-controls="tab-sharing"]') + .should('be.visible') + .click(); + }); } -export const triggerActionForFileV28 = (filename, actionId) => { - getActionButtonForFileV28(filename).click({ force: true }) - cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist') - cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).scrollIntoView().should('be.visible') - cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).click({ force: true }) +/** + * Triggers an action for a specific file. + * @param {string} filename - The name of the file. + * @param {string} actionId - The action to trigger (e.g., 'rename', 'details'). + */ +export function triggerActionForFileV28(filename, actionId) { + // Open the file's action menu + getActionButtonForFileV28(filename) + .should('be.visible') + .click({ force: true }); + + // Construct the selector for the desired action + const actionSelector = `[data-cy-files-list-row-action="${actionId}"] > button`; + + // Click the action button + cy.get(actionSelector) + .should('exist') + .should('be.visible') + .click({ force: true }); } -export const getActionButtonForFileV28 = (filename) => getActionsForFileV28(filename).find('button[aria-label="Actions"]').should('be.visible') +/** + * Retrieves the action button for a specific file. + * @param {string} filename - The name of the file. + * @returns {Cypress.Chainable>} - The action button element. + */ +export function getActionButtonForFileV28(filename) { + return getActionsForFileV28(filename) + .find('button[aria-label="Actions"]') + .should('be.visible'); +} -export const getActionsForFileV28 = (filename) => getRowForFileV28(filename).find('[data-cy-files-list-row-actions]') +/** + * Retrieves the actions container for a specific file. + * @param {string} filename - The name of the file. + * @returns {Cypress.Chainable>} - The actions container element. + */ +export function getActionsForFileV28(filename) { + return getRowForFileV28(filename) + .find('[data-cy-files-list-row-actions]'); +} -export const getRowForFileV28 = (filename) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`) +/** + * Retrieves the row element for a specific file. + * @param {string} filename - The name of the file. + * @returns {Cypress.Chainable>} - The file row element. + */ +export function getRowForFileV28(filename) { + return cy.get(`[data-cy-files-list-row-name="${filename}"]`); +} diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js new file mode 100644 index 00000000..87bf94a3 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js @@ -0,0 +1,82 @@ +/** + * @fileoverview + * Utility functions for Cypress tests interacting with OcmStub version 1. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + + +/** + * Generates an array of share assertions for validation on a page. + * This function helps validate that the expected share metadata is displayed correctly in the UI. + * + * @param {Object} expectedDetails - An object containing the expected share details. + * @param {string} expectedDetails.shareWith - The recipient of the share (e.g., "marie@ocmstub2.docker"). + * @param {string} expectedDetails.fileName - The name of the shared file (e.g., "example-file.txt"). + * @param {string} expectedDetails.shareType - The type of share (e.g., "user" or "group"). + * @param {string} expectedDetails.owner - The owner of the shared resource (e.g., "einstein@nextcloud.com"). + * @param {string} expectedDetails.sender - The sender of the shared resource (e.g., "einstein@nextcloud.com"). + * @param {string} expectedDetails.resourceType - The type of the shared resource ("file", "folder", etc.). + * @param {string} expectedDetails.protocol - The protocol used for the share (e.g., "webdav"). + * @returns {string[]} - An array of strings representing expected lines of text that should appear in the UI. + * + * @throws {Error} Throws an error if any required field in expectedDetails is missing or not a string. + * + * @example + * // Define the expected details of a federated share + * const expectedDetails = { + * shareWith: 'marie@ocmstub2.docker', + * fileName: 'example-file.txt', + * owner: 'einstein@nextcloud.com', + * sender: 'einstein@nextcloud.com', + * shareType: 'user', + * resourceType: 'file', + * protocol: 'webdav', + * }; + * + * // Generate the share assertions + * const shareAssertions = generateShareAssertions(expectedDetails); + * // The returned array can then be used with `cy.contains()` calls in Cypress tests: + * // shareAssertions.forEach(assertion => cy.contains(assertion).should('be.visible')); + */ +export function generateShareAssertions(expectedDetails) { + // Required fields that must be present and non-empty strings + const requiredFields = [ + 'shareWith', + 'fileName', + 'owner', + 'sender', + 'shareType', + 'resourceType', + 'protocol', + ]; + + // Identify any fields that are missing or invalid + const missingFields = requiredFields.filter((field) => { + return !expectedDetails[field] || typeof expectedDetails[field] !== 'string'; + }); + + // If there are any missing or invalid fields, throw an error + if (missingFields.length > 0) { + throw new Error( + `Missing or invalid fields in expectedDetails: ${missingFields.join(', ')}` + ); + } + + // Return an array of expected strings to match in the UI. + // Note: Some values (like providerId and protocol options) are partially asserted + // because their exact values may vary dynamically. We assert on the known portion of the string. + return [ + `"shareWith": "${expectedDetails.shareWith}"`, + `"name": "${expectedDetails.fileName}"`, + // Partial assertion, expecting a providerId line to appear + `"providerId":`, + `"shareType": "${expectedDetails.shareType}"`, + `"owner": "${expectedDetails.owner}"`, + `"sender": "${expectedDetails.sender}"`, + `"resourceType": "${expectedDetails.resourceType}"`, + // For protocol, we know 'name' but 'sharedSecret' may vary. + // We assert on part of the structure to ensure the protocol block is present. + `"protocol": { "name": "${expectedDetails.protocol}", "options": { "sharedSecret": "`, + ]; +} diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index 64e3671e..80ecb57b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -1,163 +1,424 @@ +/** + * @fileoverview + * Utility functions for Cypress tests interacting with ownCloud version 10. + * These functions provide abstractions for common actions such as accepting shares, + * creating federated shares, renaming files, and interacting with the file menu. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + escapeCssSelector, +} from './general'; + +/** + * Ensures that a file with the specified name exists and is visible in the file list. + * + * This function waits for the file element to appear in the DOM within the specified timeout and checks that it is visible. + * If the file does not exist or is not visible within the timeout, the test will fail with an appropriate error message. + * + * @param {string} fileName - The name of the file to check. + * @param {number} [timeout=10000] - Optional timeout in milliseconds for the check. Defaults to 10000ms. + * + * @example + * // Ensure that the file 'example.txt' exists and is visible + * ensureFileExists('example.txt'); + * + * @throws Will cause the test to fail if the file does not exist or is not visible within the timeout. + */ +export function ensureFileExists(fileName, timeout = 10000) { + // Escape special characters in the file name to safely use it in a CSS selector + const escapedFileName = escapeCssSelector(fileName); + + // Wait for the file row to exist in the DOM and be visible + cy.get(`[data-file="${escapedFileName}"]`, { timeout }) + .should('exist') + .and('be.visible'); +} + +/** + * Accepts a share dialog by clicking the "primary" button. + */ export function acceptShare() { - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Wait for the share dialog to appear and ensure it's visible + cy.get('div.oc-dialog', { timeout: 10000 }) + .should('be.visible') + .within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') + .find('button.primary') + .should('be.visible') + .click(); + }); } +/** + * Creates a share for a specific file and user. + * @param {string} fileName - The name of the file to be shared. + * @param {string} username - The username of the recipient. + * @param {string} domain - The domain of the recipient. + */ export function createShare(fileName, username, domain) { - openSharingPanel(fileName) + // Open the sharing panel for the specified file + openSharingPanel(fileName); + + // Set up an intercept for the user search API request + cy.intercept('GET', '**/apps/files_sharing/api/v1/sharees?*').as('userSearch'); - cy.get('#app-sidebar').within(() => { - cy.get('*[id^="shareWith-"]').clear() - cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch') - cy.get('*[id^="shareWith-"]').type(username + '@' + domain) - cy.wait('@userSearch') - }) + cy.get('#app-sidebar').within(() => { + // Clear and type the recipient's federated ID + cy.get('[id^="shareWith-"]') + .clear() + .type(`${username}@${domain}`); + }); - // ensure selecting remote, instead of email or group. - cy.get('*[class^=ui-autocomplete]') - .contains('span[class="autocomplete-item-typeInfo"]', 'Federated') - .should('be.visible') - .click() + // Wait for the user search API request to complete + cy.wait('@userSearch'); + + // Select the recipient as a federated user + cy.get('.ui-autocomplete') + .contains('span[class="autocomplete-item-typeInfo"]', 'Federated') + .should('be.visible') + .click(); } +/** + * Creates a group share for a file. + * @param {string} fileName - The name of the file to share. + * @param {string} group - The group to share with. + */ export function createShareGroup(fileName, group) { - openSharingPanel(fileName) + // Open the sharing panel for the specified file + openSharingPanel(fileName); + + // Set up an intercept for the group search API request + cy.intercept('GET', '**/apps/files_sharing/api/v1/sharees?*').as('groupSearch'); - cy.get('#app-sidebar').within(() => { - cy.get('*[id^="shareWith-"]').clear() - cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch') - cy.get('*[id^="shareWith-"]').type(group) - cy.wait('@userSearch') - }) + cy.get('#app-sidebar').within(() => { + // Clear and type the group name + cy.get('[id^="shareWith-"]') + .clear() + .type(group); - // ensure selecting remote, instead of email or group. - cy.get('*[class^=ui-autocomplete]') - .contains('span[class="autocomplete-item-typeInfo"]', 'Group') - .should('be.visible') - .click() + // Wait for the group search API request to complete + cy.wait('@groupSearch'); + + // Select the group from the search results + cy.get('.ui-autocomplete') + .contains('.username', group) + .should('be.visible') + .click(); + }); } +/** + * Creates a shareable link for a file and returns the copied link. + * @param {string} fileName - The name of the file to create a link for. + * @returns {Cypress.Chainable} - A chainable containing the copied share link. + */ export function createShareLink(fileName) { - openSharingPanel(fileName) - - cy.get('#app-sidebar').get('li').contains('Public Links').click(); - cy.get('#app-sidebar').get('button').contains('Create public link').click(); + // Open the sharing panel for the specified file + openSharingPanel(fileName); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .should('be.visible') - .click() + cy.get('#app-sidebar').within(() => { + // Click on "Public Links" tab + cy.contains('li', 'Public Links') + .should('be.visible') + .click(); - return cy.get('*[data-original-title^="Copy to clipboard"]') - .parent() - .find('*[class^="minify"]') - .find('input') - .invoke('val') - .then( - sometext => { - return sometext - } - ); -} + // Click on "Create public link" button + cy.contains('button', 'Create public link') + .should('be.visible') + .click(); + }); -export function createInviteToken(senderDomain) { + // Accept the share dialog if it appears + cy.get('div.oc-dialog', { timeout: 10000 }).then(($dialog) => { + if ($dialog.is(':visible')) { + cy.wrap($dialog) + .should('be.visible') + .within(() => { + cy.get('div.oc-dialog-buttonrow') + .find('button.primary') + .should('be.visible') + .click(); + }); + } + }); - cy.get('button[id="token-generator"]').should('be.visible').click() + // Extract and return the public share link + return cy.get('#app-sidebar').within(() => { + return cy.get('.shareLink input') + .invoke('val') + .then((link) => { + return link; + }); + }); +} - return cy.get('input[class="generated-token-link"]') - .invoke('val') - .then( - sometext => { - // extract token from url. - const token = sometext.replace('https://meshdir.docker/meshdir?token=', ''); +/** + * Generates an invite token for federated sharing. + * Extracts the token from the input field and returns it. + * @returns {Cypress.Chainable} - A chainable containing the extracted invite token. + */ +export function createInviteToken() { + // Ensure the "Generate Token" button is visible and click it + cy.get('button#token-generator') + .should('be.visible') + .click(); - return token.replace(`&providerDomain=${senderDomain}`, '') - } - ); + // Extract and process the token from the input field + return cy.get('input.generated-token-link') + .invoke('val') + .then((link) => { + if (!link) { + throw new Error('Token generation failed: No token found in the input field.'); + } + // Use URLSearchParams to parse the link and extract the token + const url = new URL(link); + const token = url.searchParams.get('token'); + if (!token) { + throw new Error('Token generation failed: Token parameter not found in the URL.'); + } + return token; + }); } +/** + * Generates an invite link for federated sharing. + * Combines the extracted token with the target domain to create an invite link. + * @param {string} targetDomain - The domain of the recipient. + * @returns {Cypress.Chainable} - A chainable containing the generated invite link. + */ export function createInviteLink(targetDomain) { + // Ensure the "Generate Token" button is visible and click it + cy.get('button#token-generator') + .should('be.visible') + .click(); - cy.get('button[id="token-generator"]').should('be.visible').click() + // Extract the token and construct the invite link + return cy.get('input.generated-token-link') + .invoke('val') + .then((link) => { + if (!link) { + throw new Error('Invite link generation failed: No link found in the input field.'); + } + // Extract the query parameters from the link + const url = new URL(link); + const queryParams = url.searchParams.toString(); + // Construct the invite link with the target domain + return `${targetDomain}/index.php/apps/sciencemesh/accept?${queryParams}`; + }); +} - return cy.get('input[class="generated-token-link"]') - .invoke('val') - .then( - sometext => { - // extract token from url. - const token = sometext.replace('https://meshdir.docker/meshdir?', ''); +/** + * Verifies a federated contact in the contacts table. + * @param {string} domain - The domain of the application. + * @param {string} displayName - The display name of the contact. + * @param {string} contactDomain - The expected domain of the contact. + */ +export function verifyFederatedContact(domain, displayName, contactDomain) { + cy.visit(`https://${domain}/index.php/apps/sciencemesh`); - // put target efss domain and token together. - const inviteLink = `${targetDomain}/index.php/apps/sciencemesh/accept?${token}` + cy.get('table#contact-table') + .find('p.displayname', { timeout: 10000 }) + // Ensure the display name is present + .contains(displayName) + // Traverse to the parent row + .closest('tr') + .find('p.username-provider') + // Extract the username and domain text + .invoke('text') + .then((usernameWithDomain) => { + // Extract domain after '@' + const extractedDomain = usernameWithDomain.split('@').pop(); - return inviteLink - } - ); + // Assert the domain matches + expect(extractedDomain).to.equal(contactDomain); + }); } +/** + * Accepts an invitation by clicking the accept button in the invitation dialog. + * + * This function waits for the invitation accept button to be visible and enabled within the specified timeout, + * and then clicks it to accept the invitation. + * + * @param {number} [timeout=10000] - Optional timeout in milliseconds for waiting for the accept button. Defaults to 10000ms. + * + * @example + * // Accept the invitation + * acceptScienceMeshInvitation(5000); + * + * @throws Will cause the test to fail if the accept button is not visible or interactable within the timeout. + */ +export function acceptScienceMeshInvitation(timeout = 10000) { + // Wait for the accept button to be visible and enabled + cy.get('input#accept-button', { timeout }) + .should('be.visible') + // Ensure the button is not disabled + .and('not.be.disabled') + .click(); +} + +/** + * Shares a file using ScienceMesh federated sharing. + * Opens the sharing panel, types the recipient's details, and selects the federated recipient. + * @param {string} fileName - The name of the file to share. + * @param {string} username - The username of the recipient. + * @param {string} domain - The domain of the recipient. + */ export function createScienceMeshShare(fileName, username, domain) { - openSharingPanel(fileName) + // Open the sharing panel for the file + openSharingPanel(fileName); + + // Set up an intercept for the user search API request + cy.intercept('GET', '**/apps/files_sharing/api/v1/sharees?*').as('userSearch'); - cy.get('#app-sidebar').within(() => { - cy.get('*[id^="shareWith-"]').clear() - cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch') - cy.get('*[id^="shareWith-"]').type(username + '@' + domain) - cy.wait('@userSearch') - }) + cy.get('#app-sidebar').within(() => { + // Clear the input field and type the recipient's details + cy.get('[id^="shareWith-"]') + .clear() + .type(`${username}@${domain}`); + }); - // ensure selecting ScienceMesh. - cy.get('*[class^=ui-autocomplete]') - .contains('span[class="autocomplete-item-typeInfo"]', 'Federated') - .should('be.visible', { timeout: 10000 }) - .click() + // Wait for the user search API request to complete + cy.wait('@userSearch'); + + // Select the recipient as a federated user + cy.get('.ui-autocomplete') + .contains('span[class="autocomplete-item-typeInfo"]', 'Federated') + .should('be.visible', { timeout: 10000 }) + .click(); } +/** + * Renames a file and waits for the move operation to complete. + * @param {string} fileName - The current name of the file. + * @param {string} newFileName - The new name for the file. + */ export function renameFile(fileName, newFileName) { - triggerActionInFileMenu(fileName, 'Rename') + // Trigger the "Rename" action from the file's menu + triggerActionInFileMenu(fileName, 'Rename'); + + // Intercept the MOVE API request for renaming files + cy.intercept('MOVE', '**/remote.php/dav/files/**').as('moveFile'); - // intercept the move so we can wait for it. - cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile') - getRowForFile(fileName).find('form').find('input').clear() - getRowForFile(fileName).find('form').find('input').type(`${newFileName}{enter}`) - cy.wait('@moveFile') + // Find the file row and enter the new file name + const fileRow = getRowForFile(fileName); + fileRow.find('form input') + .should('be.visible') + .clear() + .type(`${newFileName}{enter}`); + + // Wait for the move operation to complete + cy.wait('@moveFile'); } +/** + * Opens the sharing panel for a specific file. + * @param {string} fileName - The name of the file. + */ export function openSharingPanel(fileName) { - triggerActionForFile(fileName, 'Share') - - cy.get('#app-sidebar') - .find('[data-tabid="shareTabView"]') - .should('be.visible') - .click() -} - -// appId possible values are: -// 1. files -// 2. favorites -// 3. sharingin -// 4. sharingout -// 5. sharinglinks -// 6. systemtagsfilter -// 7. trashbin -export function selectAppFromLeftSide(appId) { - cy.get('div[id="app-navigation"]') - .find(`li[data-id="${CSS.escape(appId)}"]`) + // Trigger the "Share" action for the specified file + triggerActionForFile(fileName, 'Share'); + + // Ensure the sharing tab is visible and click it + cy.get('#app-sidebar') + .should('be.visible') + .within(() => { + cy.get('[data-tabid="shareTabView"]') .should('be.visible') - .click() + .click(); + }); +} + +/** + * Selects an app from the left-side navigation menu. + * @param {string} appId - The identifier of the app to select. + * Valid values: + * - "files" + * - "favorites" + * - "sharingin" + * - "sharingout" + * - "sharinglinks" + * - "systemtagsfilter" + * - "trashbin" + */ +export function selectAppFromLeftSide(appId) { + const validAppIds = [ + "files", + "favorites", + "sharingin", + "sharingout", + "sharinglinks", + "systemtagsfilter", + "trashbin" + ]; + + // Validate the appId + if (!validAppIds.includes(appId)) { + throw new Error(`Invalid appId: "${appId}". Valid options are ${validAppIds.join(", ")}.`); + } + + // Find the app in the navigation menu and click it + cy.get('div#app-navigation', { timeout: 10000 }) + .should('be.visible') + .find(`li[data-id="${appId}"]`) + .should('be.visible') + .click(); } -export function triggerActionInFileMenu (fileName, actionId) { - triggerActionForFile(fileName, 'menu') - getRowForFile(fileName).find('*[class^="filename"]').find('*[class^="fileActionsMenu"]').find(`[data-action="${CSS.escape(actionId)}"]`).should('be.visible').click() +/** + * Triggers a specific action in the file menu. + * @param {string} fileName - The name of the file. + * @param {string} actionId - The ID of the action to trigger (e.g., 'Rename'). + */ +export function triggerActionInFileMenu(fileName, actionId) { + // Open the file's action menu + triggerActionForFile(fileName, 'menu'); + + // Find and click the desired action within the file menu + cy.get('.fileActionsMenu') + .should('be.visible') + .within(() => { + cy.get(`[data-action="${actionId}"]`) + .should('be.visible') + .as('btn') + .click(); + }); } -export const triggerActionForFile = (filename, actionId) => getActionsForFile(filename).find(`[data-action="${CSS.escape(actionId)}"]`).should('be.visible').as('action-btn').click() +/** + * Triggers a specific action for a file. + * @param {string} fileName - The name of the file. + * @param {string} actionId - The ID of the action to trigger (e.g., 'Share', 'menu'). + */ +export function triggerActionForFile(fileName, actionId) { + // Find the actions container for the file and click the desired action + getActionsForFile(fileName) + .find(`[data-action="${actionId}"]`) + .should('be.visible') + .as('btn') + .click(); +} -export const getActionsForFile = (filename) => getRowForFile(filename).find('*[class^="filename"]').find('*[class^="name"]').find('*[class^="fileactions"]').should('be.visible') +/** + * Retrieves the actions container for a specific file. + * @param {string} fileName - The name of the file. + * @returns {Cypress.Chainable>} - The actions container element. + */ +export function getActionsForFile(fileName) { + return getRowForFile(fileName) + .find('.fileactions') + .should('be.visible'); +} -export const getRowForFile = (filename) => cy.get(`[data-file="${CSS.escape(filename)}"]`) +/** + * Retrieves the row element for a specific file. + * @param {string} fileName - The name of the file. + * @returns {Cypress.Chainable>} - The row element for the file. + */ +export function getRowForFile(fileName) { + return cy.get(`[data-file="${fileName}"]`).should('exist'); +} From ba4dfbd9c013ae1f7fe18faa0ce3c0b717643d74 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 9 Dec 2024 08:53:04 +0000 Subject: [PATCH 013/184] [no ci] update: author email --- dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh | 2 +- scripts/clean.sh | 2 +- scripts/db-maria.sh | 2 +- scripts/reva/cli.sh | 2 +- scripts/reva/restart_container.sh | 2 +- scripts/reva/restart_in_conatiner.sh | 2 +- scripts/switch-php.sh | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh index 8dcbab98..e7f5cf53 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------------- # Script to Test Nextcloud to Nextcloud OCM invite link flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid +# Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- diff --git a/scripts/clean.sh b/scripts/clean.sh index 17c31cef..b9d72162 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------------- # Script to Clean and Re-Initialize Docker Environment for Testing -# Author: Mohammad Mahdi Baghbani Pourvahid +# Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- diff --git a/scripts/db-maria.sh b/scripts/db-maria.sh index 414479f4..cf794e64 100755 --- a/scripts/db-maria.sh +++ b/scripts/db-maria.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------------- # Script to Execute MariaDB Commands Inside a Docker Container -# Author: Mohammad Mahdi Baghbani Pourvahid +# Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # Description: diff --git a/scripts/reva/cli.sh b/scripts/reva/cli.sh index a01244a9..7dcf62c1 100755 --- a/scripts/reva/cli.sh +++ b/scripts/reva/cli.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------------- # Script to Execute the Reva Command-Line Tool Inside a Docker Container -# Author: Mohammad Mahdi Baghbani Pourvahid +# Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # Description: diff --git a/scripts/reva/restart_container.sh b/scripts/reva/restart_container.sh index 59904af0..b921a671 100755 --- a/scripts/reva/restart_container.sh +++ b/scripts/reva/restart_container.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------------- # Script to Restart All Docker Containers with 'reva' in Their Names -# Author: Mohammad Mahdi Baghbani Pourvahid +# Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # Exit immediately if a command exits with a non-zero status, diff --git a/scripts/reva/restart_in_conatiner.sh b/scripts/reva/restart_in_conatiner.sh index 2129c7c4..c771aba8 100755 --- a/scripts/reva/restart_in_conatiner.sh +++ b/scripts/reva/restart_in_conatiner.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------------- # Script to Restart the 'reva' Process in All Docker Containers with 'reva' in Their Names -# Author: Mohammad Mahdi Baghbani Pourvahid +# Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # Exit immediately if a command exits with a non-zero status, diff --git a/scripts/switch-php.sh b/scripts/switch-php.sh index 0d9c0f86..62db1b41 100755 --- a/scripts/switch-php.sh +++ b/scripts/switch-php.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------------- # Script to Configure PHP Version Alternatives on the System -# Author: Mohammad Mahdi Baghbani Pourvahid +# Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # Exit immediately if a command exits with a non-zero status, From 77c640b363055e911c9db655dce58f6de0d49bc8 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 9 Dec 2024 09:44:11 +0000 Subject: [PATCH 014/184] [no ci] add: seafile utility file --- .../share-with/seafile-11-to-seafile-11.cy.js | 181 ++++++++++-------- .../cypress/e2e/utils/seafile-v11.js | 23 +++ 2 files changed, 125 insertions(+), 79 deletions(-) create mode 100644 cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-seafile-11.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-seafile-11.cy.js index 4fcee4fc..6cc192fb 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-seafile-11.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-seafile-11.cy.js @@ -1,92 +1,115 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in Seafile. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + dismissModalIfPresentV11, +} from '../utils/seafile-v11'; + describe('Native federated sharing functionality for Seafile', () => { - it('Send federated share from Seafile 11 to Seafile 11', () => { - // share from Seafile 1. - cy.loginSeafile('http://seafile1.docker', 'jonathan@seafile.com', 'xu') - - cy.get('*[role^="dialog"]') - .find('*[class^="modal-dialog"]') - .find('*[class^="modal-content"]') - .find('*[class^="modal-body"]') - .find('button') - .click() - - cy.get('*[id^="wrapper"]') - .find('*[class^="main-panel"]') - .find('*[class^="reach-router"]') - .find('*[class^="main-panel-center"]') - .find('*[class^="cur-view-container"]') - .find('*[class^="cur-view-content"]') - .find('table>tbody') - .eq(0) - .find('tr>td') - .eq(3).trigger('mouseover') - - cy.get('*[id^="wrapper"]') - .find('*[class^="main-panel"]') - .find('*[class^="reach-router"]') - .find('*[class^="main-panel-center"]') - .find('*[class^="cur-view-container"]') - .find('*[class^="cur-view-content"]') - .find('table>tbody') - .eq(0) - .find('tr>td') - .eq(3) - .find('*[title^="Share"]') - .click() - cy.get('*[class^="share-dialog-side"]') - .find('ul>li') + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('SEAFILE1_URL') || 'http://seafile1.docker'; + const recipientUrl = Cypress.env('SEAFILE2_URL') || 'http://seafile2.docker'; + const senderUsername = Cypress.env('SEAFILE1_USERNAME') || 'jonathan@seafile.com'; + const senderPassword = Cypress.env('SEAFILE1_PASSWORD') || 'xu'; + const recipientUsername = Cypress.env('SEAFILE2_USERNAME') || 'giuseppe@cern.ch'; + const recipientPassword = Cypress.env('SEAFILE2_PASSWORD') || 'lopresti'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'share-with-nc1-to-nc2.txt'; + + /** + * Test Case: Sending a federated share from Seafile 1 to Seafile 2. + */ + it('should successfully send a federated share of a file from Seafile 1 to Seafile 2', () => { + // Step 1: Log in to Seafile 1 + cy.loginSeafile(senderUrl, senderUsername, senderPassword); + + // Step 2: Dismiss any modals if present (e.g., welcome or info dialogs) + dismissModalIfPresentV11(); + + // Step 3: Locate the file to share and open the share menu. + // Adjust selectors as per actual DOM structure: + // - eq(0) targets the first file row. + // - eq(3) selects the 4th column cell in that row (where "Share" button is assumed). + cy.get('#wrapper .main-panel .reach-router .main-panel-center .cur-view-container .cur-view-content') + .find('table tbody') + .eq(0) // First file + .find('tr td') + .eq(3) // Column containing the Share button + .trigger('mouseover') // Hover to reveal the share button if hidden + .find('[title="Share"]') // Locate the share button by title attribute + .should('be.visible') + .click(); + + // Step 4: Select the federated sharing option + // eq(4) is the 5th item in the share dialog side menu, assumed to be "Federated Sharing" + cy.get('.share-dialog-side ul li') .eq(4) - .click() + .should('be.visible') + .click(); - cy.get('*[id^="share-to-other-server-panel"]') - .find('table>tbody') + // Step 5: Select the Seafile server from a dropdown + // Interact with the server selection dropdown + cy.get('#share-to-other-server-panel table tbody') .eq(0) - .find('tr>td') + // The dropdown trigger is assumed to be an SVG icon + .find('tr td svg') .eq(0) - .find('svg') - .click() + .should('be.visible') + .click(); - cy.get('*[role^="dialog"]') + // Select a server from the resulting dialog + // Using a regex to match a server name that starts with 'seafile' followed by word characters + cy.get('[role="dialog"]') .contains(/^seafile\w+/) - .click() + .should('be.visible') + .click(); - cy.get('*[id^="share-to-other-server-panel"]') - .find('table>tbody') + // Step 6: Enter the recipient's email and submit the share + // Within the panel, type the recipient email and click "Submit" + cy.get('#share-to-other-server-panel table tbody') .eq(0) - .find('tr>td') - .eq(1) .within(() => { - cy.get('input[class="form-control"]').type('giuseppe@cern.ch') - }) + cy.get('input.form-control') + .should('be.visible') + .type(recipientUsername); - cy.get('*[id^="share-to-other-server-panel"]') - .find('table>tbody') - .eq(0) - .find('tr>td') - .eq(3) - .contains('Submit') - .click() - - }) - - it('Receive federated share from Seafile to Seafile', () => { - cy.loginSeafile('http://seafile2.docker', 'giuseppe@cern.ch', 'lopresti') - - cy.get('*[role^="dialog"]') - .find('*[class^="modal-dialog"]') - .find('*[class^="modal-content"]') - .find('*[class^="modal-body"]') - .find('button') - .click() - - cy.get('*[id^="wrapper"]') - .find('*[class^="side-panel"]') - .find('*[class^="side-panel-center"]') - .find('*[class^="side-nav"]') - .find('*[class^="side-nav-con"]') - .find('ul>li') + cy.contains('Submit') + .should('be.visible') + .click(); + }); + + // Optional: Add assertions to verify a success message or notification, if available. + }); + + /** + * Test Case: Receiving a federated share on Seafile 2. + */ + it('should successfully receive and display a federated share of a file on Seafile 2', () => { + // Step 1: Log in to Seafile 2 + cy.loginSeafile(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Dismiss any modals if present (e.g., welcome or info dialogs) + dismissModalIfPresentV11(); + + // Step 3: Navigate to the "Received Shares" section + // eq(5) selects the 6th menu item in the sidebar assumed to be "Received Shares" + cy.get('#wrapper .side-panel .side-panel-center .side-nav .side-nav-con ul li') .eq(5) - .click() - }) -}) + .should('be.visible') + .click(); + + // Step 4: Validate that the shared file is visible + // Check that the received shares table is visible and that it has at least one row + cy.get('.received-shares-table') + .should('be.visible') + .find('tr') + .should('have.length.greaterThan', 0); + + // Optional: Further assertions could be made to verify that the expected file name appears. + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js new file mode 100644 index 00000000..c43ea8f6 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js @@ -0,0 +1,23 @@ +/** + * @fileoverview + * Utility functions for Cypress tests interacting with Seafile version 11. + * These functions provide abstractions for common actions such as accepting shares, + * creating federated shares, renaming files, and interacting with the file menu. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +// Utility function to dismiss a modal if it appears +export function dismissModalIfPresentV11() { + // No waiting; check immediately + cy.get('[role="dialog"]', { timeout: 5000 }) + .then((modals) => { + if (modals.length > 0) { + // If modal exists, close it + cy.wrap(modals) + .find('.modal-dialog .modal-content .modal-body button') + .should('be.visible') + .click(); + } + }); +} From 895ba6c6c9d803fa358798959078d0a2a37acd0c Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 9 Dec 2024 09:44:33 +0000 Subject: [PATCH 015/184] [no ci] delete: seafile to ocmstub --- .../share-with/seafile-11-to-ocmstub-v1.js | 89 ------------------- 1 file changed, 89 deletions(-) delete mode 100644 cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-ocmstub-v1.js diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-ocmstub-v1.js b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-ocmstub-v1.js deleted file mode 100644 index 50fc00e8..00000000 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-ocmstub-v1.js +++ /dev/null @@ -1,89 +0,0 @@ -describe('Native federated sharing functionality for Seafile', () => { - it('Send federated share from Seafile 11 to Seafile 11', () => { - // share from Seafile 1. - cy.loginSeafile('http://seafile1.docker', 'jonathan@seafile.com', 'xu') - - cy.get('*[role^="dialog"]') - .find('*[class^="modal-dialog"]') - .find('*[class^="modal-content"]') - .find('*[class^="modal-body"]') - .find('button') - .click() - - cy.get('*[id^="wrapper"]') - .find('*[class^="main-panel"]') - .find('*[class^="reach-router"]') - .find('*[class^="main-panel-center"]') - .find('*[class^="cur-view-container"]') - .find('*[class^="cur-view-content"]') - .find('table>tbody') - .eq(0) - .find('tr>td') - .eq(3).trigger('mouseover') - - cy.get('*[id^="wrapper"]') - .find('*[class^="main-panel"]') - .find('*[class^="reach-router"]') - .find('*[class^="main-panel-center"]') - .find('*[class^="cur-view-container"]') - .find('*[class^="cur-view-content"]') - .find('table>tbody') - .eq(0) - .find('tr>td') - .eq(3) - .find('*[title^="Share"]') - .click() - - cy.get('*[class^="share-dialog-side"]') - .find('ul>li') - .eq(4) - .click() - - cy.get('*[id^="share-to-other-server-panel"]') - .find('table>tbody') - .eq(0) - .find('tr>td') - .eq(0) - .find('svg') - .click() - - cy.get('*[role^="dialog"]') - .contains(/^seafile\w+/) - .click() - - cy.get('*[id^="share-to-other-server-panel"]') - .find('table>tbody') - .eq(0) - .find('tr>td') - .eq(1) - .within(() => { - cy.get('input[class="form-control"]').type('giuseppe@cern.ch') - }) - - cy.get('*[id^="share-to-other-server-panel"]') - .find('table>tbody') - .eq(0) - .find('tr>td') - .eq(3) - .contains('Submit') - .click() - - }) - - it('Receive federated share from ownCloud v10 to OcmStub 1.0', () => { - // accept share from OcmStub 2. - cy.loginOcmStub('https://ocmstub2.docker/?') - - cy.contains('"shareWith": "michiel@https://ocmstub2.docker"').should('be.visible') - cy.contains('"shareType": "user"').should('be.visible') - cy.contains('"name": "nc1-to-os2-share.txt"').should('be.visible') - cy.contains('"resourceType": "file"').should('be.visible') - cy.contains('"owner": "einstein@https://seafile1.docker/"').should('be.visible') - cy.contains('"sharedBy": "einstein@https://seafile1.docker/"').should('be.visible') - cy.contains('"ownerDisplayName": "einstein"').should('be.visible') - cy.contains('"description": ""').should('be.visible') - cy.contains('"shareWith": "michiel@https://ocmstub2.docker"').should('be.visible') - cy.contains('"protocol": { "name": "webdav", "options": { "sharedSecret": "').should('be.visible') - cy.contains('"permissions": "{http://open-cloud-mesh.org/ns}share-permissions"').should('be.visible') - }) -}) From b3c20a8461a1c0f1cd1eb2715e48ac1092e94365 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 9 Dec 2024 10:12:33 +0000 Subject: [PATCH 016/184] [no ci] refactor: login tests --- .../cypress/e2e/login/ocis.cy.js | 27 +- .../cypress/e2e/login/seafile.cy.js | 27 +- .../cypress/support/commands.js | 241 +++++++++++------- 3 files changed, 196 insertions(+), 99 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/login/ocis.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/ocis.cy.js index 6c82fb0d..d9879eb1 100644 --- a/cypress/ocm-test-suite/cypress/e2e/login/ocis.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/login/ocis.cy.js @@ -1,5 +1,22 @@ -describe('Login oCIS', () => { - it('Login test for oCIS', () => { - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') - }) -}) +/** + * @fileoverview + * Cypress test suite for testing the login functionality of oCIS. + * This suite contains tests to validate successful login functionality using valid credentials. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +describe('oCIS Login Tests', () => { + /** + * Test Case: Validates successful login to oCIS. + * This test logs into oCIS using valid credentials and checks for a successful login state. + */ + it('should successfully log into oCIS with valid credentials', () => { + // Define the oCIS instance URL and credentials from environment variables or use default values + const ocisUrl = Cypress.env('OCIS1_URL') || 'https://ocis1.docker'; + const username = Cypress.env('OCIS1_USERNAME') || 'einstein'; + const password = Cypress.env('OCIS1_PASSWORD') || 'relativity'; + + cy.loginOcis(ocisUrl, username, password); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/login/seafile.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/seafile.cy.js index bd159639..7d96fc1d 100644 --- a/cypress/ocm-test-suite/cypress/e2e/login/seafile.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/login/seafile.cy.js @@ -1,5 +1,22 @@ -describe('Login Seafile', () => { - it('Login test for Seafile', () => { - cy.loginSeafile('http://seafile1.docker', 'jonathan@seafile.com', 'xu') - }) - }) +/** + * @fileoverview + * Cypress test suite for testing the login functionality of Seafile. + * This suite contains tests to validate successful login functionality using valid credentials. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +describe('Seafile Login Tests', () => { + /** + * Test Case: Validates successful login to Seafile. + * This test logs into Seafile using valid credentials and checks for a successful login state. + */ + it('should successfully log into Seafile with valid credentials', () => { + // Define the Seafile instance URL and credentials from environment variables or use default values + const seafileUrl = Cypress.env('SEAFILE1_URL') || 'https://seafile1.docker'; + const username = Cypress.env('SEAFILE1_USERNAME') || 'jonathan@seafile.com'; + const password = Cypress.env('SEAFILE1_PASSWORD') || 'xu'; + + cy.loginOcis(seafileUrl, username, password); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/support/commands.js b/cypress/ocm-test-suite/cypress/support/commands.js index 5f01e31e..2f0efb2d 100644 --- a/cypress/ocm-test-suite/cypress/support/commands.js +++ b/cypress/ocm-test-suite/cypress/support/commands.js @@ -1,100 +1,163 @@ +/** + * Cypress custom commands for logging into various platforms. + */ + +/** + * Login to Nextcloud Core. + * Logs into Nextcloud using provided credentials, ensuring the login page is visible before interacting with it. + * + * @param {string} url - The URL of the Nextcloud instance. + * @param {string} username - The username for login. + * @param {string} password - The password for login. + */ Cypress.Commands.add('loginNextcloudCore', (url, username, password) => { - cy.visit(url) - - // login page is visible in browser. - cy.get('form[name="login"]', { timeout: 10000 }).should('be.visible') - - // login with username and password. - cy.get('form[name="login"]').within(() => { - cy.get('input[name="user"]').type(username) - cy.get('input[name="password"]').type(password) - cy.contains('button[data-login-form-submit]', 'Log in').click() - }) -}) - + cy.visit(url); + + // Ensure the login page is visible + cy.get('form[name="login"]', { timeout: 10000 }).should('be.visible'); + + // Fill in login credentials and submit + cy.get('form[name="login"]').within(() => { + cy.get('input[name="user"]').type(username); + cy.get('input[name="password"]').type(password); + cy.contains('button[data-login-form-submit]', 'Log in').click(); + }); +}); + +/** + * Login to Nextcloud and navigate to the files app. + * Extends the core login functionality by verifying the dashboard and navigating to the files app. + * + * @param {string} url - The URL of the Nextcloud instance. + * @param {string} username - The username for login. + * @param {string} password - The password for login. + */ Cypress.Commands.add('loginNextcloud', (url, username, password) => { - cy.loginNextcloudCore(url, username, password) - - // dashboard should be visible. - cy.url({ timeout: 10000 }).should('match', /apps\/dashboard(\/|$)/) - - // open files app. - cy.get('header[id="header"]') - .find('div[class="header-left"]') - .find('nav[class="app-menu"]') - .find('ul[class="app-menu-main"]') - .find('li[data-app-id="files"]') - .click() - - // files app should be visible. - cy.url({ timeout: 10000 }).should('match', /apps\/files(\/|$)/) -}) - + cy.loginNextcloudCore(url, username, password); + + // Verify dashboard visibility + cy.url({ timeout: 10000 }).should('match', /apps\/dashboard(\/|$)/); + + // Navigate to the files app + cy.get('header[id="header"] nav.app-menu ul.app-menu-main li[data-app-id="files"]') + .should('be.visible') + .click(); + + // Verify files app visibility + cy.url({ timeout: 10000 }).should('match', /apps\/files(\/|$)/); +}); + +/** + * Login to Seafile. + * Logs into Seafile using provided credentials. + * + * @param {string} url - The URL of the Seafile instance. + * @param {string} username - The username for login. + * @param {string} password - The password for login. + */ Cypress.Commands.add('loginSeafile', (url, username, password) => { - cy.visit(url) - - // login page is visible in browser. - cy.get('*[id^="wrapper"]').find('*[id^="log-in-panel"]').find('*[id^="login-form"]', { timeout: 10000 }).should('be.visible') - - // login with username and password. - cy.get('*[id^="wrapper"]').find('*[id^="log-in-panel"]').find('*[id^="login-form"]').within(() => { - cy.get('input[name="login"]').type(username) - cy.get('input[name="password"]').type(password) - cy.get('*[type=submit]').click() - }) -}) - + cy.visit(url); + + // Ensure the login page is visible + cy.get('#wrapper #log-in-panel #login-form', { timeout: 10000 }).should('be.visible'); + + // Fill in login credentials and submit + cy.get('#wrapper #log-in-panel #login-form').within(() => { + cy.get('input[name="login"]').type(username); + cy.get('input[name="password"]').type(password); + cy.get('button[type="submit"]').click(); + }); +}); + +/** + * Login to ownCloud Core. + * Logs into ownCloud using provided credentials. + * + * @param {string} url - The URL of the ownCloud instance. + * @param {string} username - The username for login. + * @param {string} password - The password for login. + */ Cypress.Commands.add('loginOwncloudCore', (url, username, password) => { - cy.visit(url) - - // login page is visible in browser. - cy.get('form[name="login"]', { timeout: 10000 }).should('be.visible') - - // login with username and password. - cy.get('form[name="login"]').within(() => { - cy.get('input[name="user"]').type(username) - cy.get('input[name="password"]').type(password) - cy.get('button[id="submit"]').click() - }) -}) - + cy.visit(url); + + // Ensure the login page is visible + cy.get('form[name="login"]', { timeout: 10000 }).should('be.visible'); + + // Fill in login credentials and submit + cy.get('form[name="login"]').within(() => { + cy.get('input[name="user"]').type(username); + cy.get('input[name="password"]').type(password); + cy.get('button[id="submit"]').click(); + }); +}); + +/** + * Login to ownCloud and navigate to the files app. + * Extends the core login functionality by verifying the files app is accessible. + * + * @param {string} url - The URL of the ownCloud instance. + * @param {string} username - The username for login. + * @param {string} password - The password for login. + */ Cypress.Commands.add('loginOwncloud', (url, username, password) => { - cy.loginOwncloudCore(url, username, password) - - // files app should be visible. - cy.url({ timeout: 10000 }).should('match', /apps\/files(\/|$)/) -}) - + cy.loginOwncloudCore(url, username, password); + + // Verify files app visibility + cy.url({ timeout: 10000 }).should('match', /apps\/files(\/|$)/); +}); + +/** + * Login to OCIS Core. + * Logs into OCIS using provided credentials. + * + * @param {string} url - The URL of the OCIS instance. + * @param {string} username - The username for login. + * @param {string} password - The password for login. + */ Cypress.Commands.add('loginOcisCore', (url, username, password) => { - cy.visit(url) - - // login page is visible in browser. - cy.get('form[class="oc-login-form"]', { timeout: 10000 }).should('be.visible') - - // login with username and password. - cy.get('form[class="oc-login-form"]').within(() => { - cy.get('input[id="oc-login-username"]').type(username) - cy.get('input[id="oc-login-password"]').type(password) - cy.get('button[type="submit"]').click() - }) -}) - + cy.visit(url); + + // Ensure the login page is visible + cy.get('form.oc-login-form', { timeout: 10000 }).should('be.visible'); + + // Fill in login credentials and submit + cy.get('form.oc-login-form').within(() => { + cy.get('input#oc-login-username').type(username); + cy.get('input#oc-login-password').type(password); + cy.get('button[type="submit"]').click(); + }); +}); + +/** + * Login to OCIS and navigate to the personal files app. + * Extends the core login functionality by verifying the personal files app is accessible. + * + * @param {string} url - The URL of the OCIS instance. + * @param {string} username - The username for login. + * @param {string} password - The password for login. + */ Cypress.Commands.add('loginOcis', (url, username, password) => { - cy.loginOcisCore(url, username, password) - - // files app should be visible. - cy.url({ timeout: 10000 }).should('match', /files\/spaces\/personal(\/|$)/) -}) - + cy.loginOcisCore(url, username, password); + + // Verify personal files app visibility + cy.url({ timeout: 10000 }).should('match', /files\/spaces\/personal(\/|$)/); +}); + +/** + * Login to OCM Stub. + * Navigates to the OCM Stub login page and logs in using a default flow. + * + * @param {string} url - The URL of the OCM Stub instance. + */ Cypress.Commands.add('loginOcmStub', (url) => { - cy.visit(url) + cy.visit(`${url}/?`); - // login buton is visible in browser. - cy.get('input[value="Log in"]', { timeout: 10000 }).should('be.visible') + // Ensure the login button is visible + cy.get('input[value="Log in"]', { timeout: 10000 }).should('be.visible'); - // login with button. - cy.get('input[value="Log in"]').click() + // Perform login by clicking the button + cy.get('input[value="Log in"]').click(); - // files app should be visible. - cy.url({ timeout: 10000 }).should('match', /\/?session=active/) -}) + // Verify session activation + cy.url({ timeout: 10000 }).should('match', /\/?session=active/); +}); From 8207f73cb560bbaf9f2d96839eb00760eedb7e6a Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 9 Dec 2024 10:13:19 +0000 Subject: [PATCH 017/184] add: note about intercepting ocis request with ocmstub --- dev/ocm-test-suite/note.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 dev/ocm-test-suite/note.txt diff --git a/dev/ocm-test-suite/note.txt b/dev/ocm-test-suite/note.txt new file mode 100644 index 00000000..c260d32a --- /dev/null +++ b/dev/ocm-test-suite/note.txt @@ -0,0 +1,3 @@ + +// command to test ocis post invite flow +docker run --detach --network=testnet --name="revanextcloud1.docker" -e HOST="revanextcloud1" -v "./docker/tls/certificates/revanextcloud1.crt:/tls/revanextcloud1.crt" -v "./docker/tls/certificates/revanextcloud1.key:/tls/revanextcloud1.key" pondersource/dev-stock-ocmstub:latest \ No newline at end of file From 435e8eee7c7af29c0826a6477bccd27835cf8e79 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 9 Dec 2024 11:01:59 +0000 Subject: [PATCH 018/184] fix: function signature --- .../invite-link/nextcloud-v27-to-ocis-5.cy.js | 2 +- .../invite-link/ocis-5-to-nextcloud-v27.cy.js | 42 +++++++++---------- .../invite-link/owncloud-v10-to-ocis-5.cy.js | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-ocis-5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-ocis-5.cy.js index 3514d6ba..9ad20a1e 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-ocis-5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-ocis-5.cy.js @@ -17,7 +17,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for oCIS', cy.loginNextcloud('https://nextcloud1.docker', 'marie', 'radioactivity') cy.visit('https://nextcloud1.docker/index.php/apps/sciencemesh/contacts') - createInviteTokenV27('revanextcloud1.docker').then( + createInviteTokenV27().then( (result) => { // save invite link to file. cy.writeFile('invite-link-nc-ocis.txt', result) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-nextcloud-v27.cy.js index 507af453..14b5d28b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-nextcloud-v27.cy.js @@ -46,33 +46,33 @@ describe('Invite link federated sharing via ScienceMesh functionality for oCIS', }) }) - it('Send ScienceMesh share from oCIS v5 to Nextcloud v27', () => { - // share from oCIS 1. - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') + // it('Send ScienceMesh share from oCIS v5 to Nextcloud v27', () => { + // // share from oCIS 1. + // cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') - createTextFileV5('invite-link-ocis-nc.txt', 'Hello World!') + // createTextFileV5('invite-link-ocis-nc.txt', 'Hello World!') - openFilesAppV5() + // openFilesAppV5() - createShareV5('invite-link-ocis-nc.txt', 'marie') + // createShareV5('invite-link-ocis-nc.txt', 'marie') - cy.wait(5000) - }) + // cy.wait(5000) + // }) - it('Receive ScienceMesh share from oCIS v5 to Nextcloud v27', () => { - // accept share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'marie', 'radioactivity') + // it('Receive ScienceMesh share from oCIS v5 to Nextcloud v27', () => { + // // accept share from Nextcloud 1. + // cy.loginNextcloud('https://nextcloud1.docker', 'marie', 'radioactivity') - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // cy.get('div[class="oc-dialog"]', { timeout: 10000 }) + // .should('be.visible') + // .find('*[class^="oc-dialog-buttonrow"]') + // .find('button[class="primary"]') + // .click() - navigationSwitchLeftSideV27('Open navigation') - selectAppFromLeftSideV27('shareoverview') - navigationSwitchLeftSideV27('Close navigation') + // navigationSwitchLeftSideV27('Open navigation') + // selectAppFromLeftSideV27('shareoverview') + // navigationSwitchLeftSideV27('Close navigation') - cy.get('[data-file="invite-link-ocis-nc.txt"]', { timeout: 10000 }).should('be.visible') - }) + // cy.get('[data-file="invite-link-ocis-nc.txt"]', { timeout: 10000 }).should('be.visible') + // }) }) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-ocis-5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-ocis-5.cy.js index 89f16404..9f9f15e6 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-ocis-5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-ocis-5.cy.js @@ -16,7 +16,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') cy.visit('https://owncloud1.docker/index.php/apps/sciencemesh/') - createInviteToken('revaowncloud1.docker').then( + createInviteToken().then( (result) => { // save invite link to file. cy.writeFile('invite-link-oc-ocis.txt', result) From 43e37f6228487c1bfc86d5b389aafe76c20b45a8 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 9 Dec 2024 11:44:32 +0000 Subject: [PATCH 019/184] [no ci] refactor: tagging releases and undoing the tag if necessary --- release/tag-release.py | 347 ++++++++++++++++++++++++++++------------- 1 file changed, 242 insertions(+), 105 deletions(-) diff --git a/release/tag-release.py b/release/tag-release.py index 49331d48..bcd90d87 100755 --- a/release/tag-release.py +++ b/release/tag-release.py @@ -1,112 +1,249 @@ #!/usr/bin/env python3 -# Python Standard Library import os import sys import subprocess - -args = sys.argv - -platform_tag = args[1] if not args[1] == "none" else "" -pre_release_tag = f"-{args[2]}" if not args[2] == "none" else "" -directory_name = args[3] - -# get path to this file's directory, then go one directory up. -file_path = os.path.abspath(os.path.dirname(__file__)) -base_path = os.path.abspath(os.path.dirname(file_path)) -project_path = os.path.join(base_path, f"{directory_name}") -version_file_path = os.path.join(project_path, "appinfo", "info.xml") - -# open version file. -with open(version_file_path) as file: - version_file = file.readlines() - -# set version and version_info to None, so if we didn't find -# a version in info.xml, we can throw an error. -version = None -version_info = None - -# find version -for line in version_file: - if "" in line: - # find versioninside info.xml and reformat it to - # standard x.y.z version format - tuple_left = line.index(">") - tuple_right = line.index("/") - version = line[tuple_left + 1:tuple_right - - 1].replace(",", ".").replace(" ", "") - # creat a list from x.y.z string which has [x, y, z] - # notice that x, y , z must be converted to integer - version_info = [int(number) for number in version.split(".")] - -# throw error if version not found -if not version or not version_info: - raise ValueError("ERROR: version not found at info.xml.") - -print("This program will tag a new release of the app\n" - + "and it will push the new tag to github.\n") - -# read and convert to integer. -print("Version is in X.Y.Z form.\n" - "X is version major, Y is version minor, Z is version minor.\n\n") - -print(f"Current version is {version} .\n\n") - -new_major = int(input("Enter version major number:\n")) -new_minor = int(input("Enter version minor number:\n")) -new_patch = int(input("Enter version patch number:\n")) - -new_version = ".".join(map(str, [new_major, new_minor, new_patch])) - -# check version to be bigger than last version. -if new_version == version: - raise ValueError("Version can't be same as current version!") - -if new_major < version_info[0]: - raise ValueError( - "Major version can't be less than the current major version!") -elif new_major > version_info[0]: - pass -elif new_minor < version_info[1]: - raise ValueError( - "Minor version can't be less than the current minor version!") -elif new_minor > version_info[1]: - pass -elif new_patch < version_info[2]: - raise ValueError( - "Patch version can't be less than the current patch version!") - - -# creat an empty list for new info.xml file -print("Creating new version. \n\n") - -new_xml_file = list() - -# write new version_info and in info.xml. -new_version_info = f" {new_major}.{new_minor}.{new_patch}\n" - -# read current info.xml, and update version -# then append to new_xml_file list. -with open(version_file_path, "r") as file: - lines = file.readlines() - for line in lines: - if "" in line: - new_xml_file.append(new_version_info) +import xml.etree.ElementTree as ET +import argparse + +def parse_arguments(): + """ + Parses command-line arguments using argparse. + + Returns: + argparse.Namespace: Parsed command-line arguments. + """ + parser = argparse.ArgumentParser(description="Version tagging script.") + parser.add_argument("platform_tag", help="Platform tag") + parser.add_argument("pre_release_tag", help="Pre-release tag (or 'none')") + parser.add_argument("directory_name", help="Directory name of the project") + parser.add_argument("--new-version", help="New version number in X.Y.Z format") + parser.add_argument("--auto-push", action="store_true", help="Automatically push changes without confirmation") + return parser.parse_args() + +def get_version_info(version_file_path): + """ + Reads the 'info.xml' file and extracts the version information. + + Args: + version_file_path (str): Path to the 'info.xml' file. + + Returns: + tuple: A tuple containing the version string and version tuple. + + Raises: + FileNotFoundError: If the 'info.xml' file does not exist. + ValueError: If the version tag is missing or malformed. + """ + try: + tree = ET.parse(version_file_path) + root = tree.getroot() + version_element = root.find('version') + if version_element is None or version_element.text is None: + raise ValueError("Version tag not found or empty in info.xml.") + version = version_element.text.strip() + version_info = tuple(map(int, version.split('.'))) + return version, version_info + except FileNotFoundError: + raise FileNotFoundError(f"Version file not found: {version_file_path}") + except ET.ParseError as e: + raise ValueError("Malformed XML in info.xml.") from e + except ValueError as e: + raise ValueError(f"Malformed version in info.xml: {e}") from e + +def validate_version_format(version_tuple): + """ + Validates the format of the version tuple. + + Args: + version_tuple (tuple): The version tuple to validate. + + Raises: + ValueError: If the version tuple does not have exactly three integer components. + """ + if len(version_tuple) != 3 or not all(isinstance(x, int) for x in version_tuple): + raise ValueError("Version must be in X.Y.Z format (e.g., 1.0.0).") + +def validate_new_version(new_version_info, current_version_info): + """ + Validates that the new version is greater than the current version. + + Args: + new_version_info (tuple): New version tuple. + current_version_info (tuple): Current version tuple. + + Raises: + ValueError: If the new version is not greater than the current version. + """ + if new_version_info <= current_version_info: + raise ValueError("New version must be greater than the current version!") + +def update_version_file(version_file_path, new_version): + """ + Updates the 'info.xml' file with the new version. + + Args: + version_file_path (str): Path to the 'info.xml' file. + new_version (str): New version string. + + Raises: + RuntimeError: If the version file cannot be updated. + """ + try: + tree = ET.parse(version_file_path) + root = tree.getroot() + version_element = root.find('version') + if version_element is None: + raise ValueError("Version tag not found in info.xml.") + version_element.text = new_version + tree.write(version_file_path, encoding='UTF-8', xml_declaration=True) + except Exception as e: + raise RuntimeError("Failed to update version file.") from e + +def git_commit_tag(project_path, version_file_path, tag): + """ + Performs Git commit and tagging operations. + + Args: + project_path (str): Path to the project directory. + version_file_path (str): Path to the version file. + tag (str): Tag name to create. + + Raises: + RuntimeError: If any Git operation fails. + """ + try: + # Stage the version file + subprocess.check_call(["git", "-C", project_path, "add", version_file_path]) + # Commit the change + subprocess.check_call(["git", "-C", project_path, "commit", "-m", f"version: {tag}"]) + # Create a new tag + subprocess.check_call(["git", "-C", project_path, "tag", tag]) + except subprocess.CalledProcessError as e: + raise RuntimeError("Git operation failed.") from e + +def git_revert_tag(project_path, tag): + """ + Reverts the Git tag and commit if the push is declined. + + Args: + project_path (str): Path to the project directory. + tag (str): Tag name to delete. + + Raises: + RuntimeError: If reverting the Git tag and commit fails. + """ + try: + # Delete the tag + subprocess.check_call(["git", "-C", project_path, "tag", "-d", tag]) + # Undo the last commit + subprocess.check_call(["git", "-C", project_path, "reset", "--hard", "HEAD~1"]) + except subprocess.CalledProcessError as e: + raise RuntimeError("Failed to revert Git tag and commit.") from e + +def git_push(project_path, tag): + """ + Pushes the commit and tag to the remote repository. + + Args: + project_path (str): Path to the project directory. + tag (str): Tag name to push. + + Raises: + RuntimeError: If the Git push fails. + """ + try: + # Push the commit and the tag + subprocess.check_call(["git", "-C", project_path, "push", "origin", "HEAD"]) + subprocess.check_call(["git", "-C", project_path, "push", "origin", tag]) + except subprocess.CalledProcessError as e: + raise RuntimeError("Git push failed.") from e + +def main(): + """ + Main function that orchestrates the version tagging process. + + Steps: + - Parses command-line arguments. + - Reads the current version from 'info.xml'. + - Obtains the new version (either via arguments or user input). + - Validates the new version. + - Updates 'info.xml' with the new version. + - Commits the change and creates a Git tag. + - Optionally pushes the changes to the remote repository. + """ + try: + # Parse command-line arguments + args = parse_arguments() + platform_tag = args.platform_tag + pre_release_tag = args.pre_release_tag + directory_name = args.directory_name + new_version_arg = args.new_version + auto_push = args.auto_push + + # Build pre-release suffix + pre_release_suffix = f"-{pre_release_tag}" if pre_release_tag != "none" else "" + + # Derive paths + base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + project_path = os.path.join(base_path, directory_name) + version_file_path = os.path.join(project_path, "appinfo", "info.xml") + + # Read current version + current_version, current_version_info = get_version_info(version_file_path) + print(f"Current version: {current_version}") + + # Get new version from the user or arguments + if new_version_arg: + new_version = new_version_arg.strip() + new_version_info = tuple(map(int, new_version.split('.'))) else: - new_xml_file.append(line) - -# write updated content from new_xml_file -# back into info.xml file -with open(version_file_path, "w+") as file: - file.writelines(new_xml_file) - -# do git commit and tag and push to upstreams -print("Commit and Tag and Push to upstream. \n\n") - -tag = f"v{new_version}-{platform_tag}{pre_release_tag}" + print("Enter the new version in X.Y.Z format.") + new_major = int(input("Enter major version: ")) + new_minor = int(input("Enter minor version: ")) + new_patch = int(input("Enter patch version: ")) + new_version_info = (new_major, new_minor, new_patch) + new_version = '.'.join(map(str, new_version_info)) + + # Validate new version + validate_version_format(new_version_info) + validate_new_version(new_version_info, current_version_info) + + # Update version in the file + update_version_file(version_file_path, new_version) + print("Version updated successfully.") + + # Create tag and commit + tag = f"v{new_version}-{platform_tag}{pre_release_suffix}" + git_commit_tag(project_path, version_file_path, tag) + print(f"Tag '{tag}' created.") + + # Confirm push + if auto_push: + confirm_push = "yes" + else: + confirm_push = input("Do you want to push the changes to the repository? (yes/no): ").strip().lower() -subprocess.call( - f"cd {project_path} && git commit {version_file_path} -m \"version: {tag}\"", shell=True) -subprocess.call(f"cd {project_path} && git tag \"{tag}\"", shell=True) -subprocess.call( - f"cd {project_path} && git push origin HEAD \"{tag}\"", shell=True) + if confirm_push == "yes": + git_push(project_path, tag) + print(f"Tag '{tag}' pushed to the repository.") + else: + print("Push declined. Reverting tag and commit.") + git_revert_tag(project_path, tag) + print("Reverted tag and commit.") + + except ValueError as ve: + print(f"Error: {ve}") + sys.exit(1) + except FileNotFoundError as fe: + print(f"Error: {fe}") + sys.exit(1) + except RuntimeError as re: + print(f"Error: {re}") + sys.exit(1) + except Exception as e: + print(f"Unexpected error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() From 8eaecb9c6d372379ba65cbacae17988e80ce2158 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 14 Dec 2024 11:24:53 +0000 Subject: [PATCH 020/184] update: nextcloud versions --- .github/workflows/login-nextcloud-v28.yml | 4 +-- .../workflows/share-link-nc-v27-nc-v28.yml | 4 +-- .../workflows/share-link-nc-v28-nc-v27.yml | 4 +-- .../workflows/share-link-nc-v28-nc-v28.yml | 6 ++-- .../workflows/share-link-nc-v28-oc-v10.yml | 4 +-- .../workflows/share-link-oc-v10-nc-v28.yml | 4 +-- .../workflows/share-with-nc-v27-nc-v28.yml | 4 +-- .../workflows/share-with-nc-v28-nc-v27.yml | 4 +-- .../workflows/share-with-nc-v28-nc-v28.yml | 6 ++-- .../workflows/share-with-nc-v28-oc-v10.yml | 4 +-- .github/workflows/share-with-nc-v28-os-v1.yml | 4 +-- .../workflows/share-with-oc-v10-nc-v28.yml | 4 +-- README.md | 28 +++++++++---------- dev/ocm-nextcloud.sh | 4 +-- .../invite-link/nextcloud-ocis.sh | 2 +- .../invite-link/nextcloud-owncloud.sh | 2 +- .../invite-link/ocis-nextcloud.sh | 2 +- .../invite-link/owncloud-nextcloud.sh | 4 +-- dev/ocm-test-suite/login/nextcloud.sh | 2 +- .../share-link/nextcloud-nextcloud.sh | 4 +-- .../share-link/nextcloud-owncloud.sh | 2 +- .../share-link/owncloud-nextcloud.sh | 4 +-- .../share-with/nextcloud-ocmstub.sh | 2 +- .../share-with/nextcloud-owncloud.sh | 2 +- .../share-with/nextcloud-seafile.sh | 2 +- .../share-with/owncloud-nextcloud.sh | 4 +-- docker/build/ocm-test-suite.sh | 8 +++--- docker/build/php-base.sh | 2 +- docker/build/sciencemesh.sh | 8 +++--- docker/build/solid.sh | 10 +++---- docker/pull/all.sh | 4 +-- docker/pull/ocm-test-suite.sh | 4 +-- docker/pull/ocm-test-suite/nextcloud.sh | 2 +- docker/pull/ocm-test-suite/ocmstub.sh | 2 +- docker/pull/sciencemesh.sh | 4 +-- docker/push/all.sh | 6 ++-- 36 files changed, 83 insertions(+), 83 deletions(-) diff --git a/.github/workflows/login-nextcloud-v28.yml b/.github/workflows/login-nextcloud-v28.yml index 14037eaf..3b3fae98 100644 --- a/.github/workflows/login-nextcloud-v28.yml +++ b/.github/workflows/login-nextcloud-v28.yml @@ -1,4 +1,4 @@ -name: OCM Test Login Nextcloud v28.0.12 +name: OCM Test Login Nextcloud v28.0.14 # Controls when the action will run. on: @@ -29,7 +29,7 @@ jobs: efss: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] diff --git a/.github/workflows/share-link-nc-v27-nc-v28.yml b/.github/workflows/share-link-nc-v27-nc-v28.yml index d1c10885..f07b5a26 100644 --- a/.github/workflows/share-link-nc-v27-nc-v28.yml +++ b/.github/workflows/share-link-nc-v27-nc-v28.yml @@ -1,4 +1,4 @@ -name: OCM Test Share Link NC v27.1.11 to NC v28.0.12 +name: OCM Test Share Link NC v27.1.11 to NC v28.0.14 # Controls when the action will run. on: @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] diff --git a/.github/workflows/share-link-nc-v28-nc-v27.yml b/.github/workflows/share-link-nc-v28-nc-v27.yml index 374627a0..291b671f 100644 --- a/.github/workflows/share-link-nc-v28-nc-v27.yml +++ b/.github/workflows/share-link-nc-v28-nc-v27.yml @@ -1,4 +1,4 @@ -name: OCM Test Share Link NC v28.0.12 to NC v27.1.11 +name: OCM Test Share Link NC v28.0.14 to NC v27.1.11 # Controls when the action will run. on: @@ -29,7 +29,7 @@ jobs: sender: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] receiver: [ diff --git a/.github/workflows/share-link-nc-v28-nc-v28.yml b/.github/workflows/share-link-nc-v28-nc-v28.yml index 754f0a69..dd9fc147 100644 --- a/.github/workflows/share-link-nc-v28-nc-v28.yml +++ b/.github/workflows/share-link-nc-v28-nc-v28.yml @@ -1,4 +1,4 @@ -name: OCM Test Share Link NC v28.0.12 to NC v28.0.12 +name: OCM Test Share Link NC v28.0.14 to NC v28.0.14 # Controls when the action will run. on: @@ -29,13 +29,13 @@ jobs: sender: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] receiver: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] diff --git a/.github/workflows/share-link-nc-v28-oc-v10.yml b/.github/workflows/share-link-nc-v28-oc-v10.yml index 02c490eb..787542d0 100644 --- a/.github/workflows/share-link-nc-v28-oc-v10.yml +++ b/.github/workflows/share-link-nc-v28-oc-v10.yml @@ -1,4 +1,4 @@ -name: OCM Test Share Link NC v28.0.12 to OC v10.15.0 +name: OCM Test Share Link NC v28.0.14 to OC v10.15.0 # Controls when the action will run. on: @@ -29,7 +29,7 @@ jobs: sender: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] receiver: [ diff --git a/.github/workflows/share-link-oc-v10-nc-v28.yml b/.github/workflows/share-link-oc-v10-nc-v28.yml index 47ae6552..dddda453 100644 --- a/.github/workflows/share-link-oc-v10-nc-v28.yml +++ b/.github/workflows/share-link-oc-v10-nc-v28.yml @@ -1,4 +1,4 @@ -name: OCM Test Share Link OC v10.15.0 to NC v28.0.12 +name: OCM Test Share Link OC v10.15.0 to NC v28.0.14 # Controls when the action will run. on: @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] diff --git a/.github/workflows/share-with-nc-v27-nc-v28.yml b/.github/workflows/share-with-nc-v27-nc-v28.yml index 615f5c97..cbb3b023 100644 --- a/.github/workflows/share-with-nc-v27-nc-v28.yml +++ b/.github/workflows/share-with-nc-v27-nc-v28.yml @@ -1,4 +1,4 @@ -name: OCM Test Share With NC v27.1.11 to NC v28.0.12 +name: OCM Test Share With NC v27.1.11 to NC v28.0.14 # Controls when the action will run. on: @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] diff --git a/.github/workflows/share-with-nc-v28-nc-v27.yml b/.github/workflows/share-with-nc-v28-nc-v27.yml index e7ba2215..08470aa1 100644 --- a/.github/workflows/share-with-nc-v28-nc-v27.yml +++ b/.github/workflows/share-with-nc-v28-nc-v27.yml @@ -1,4 +1,4 @@ -name: OCM Test Share With NC v28.0.12 to NC v27.1.11 +name: OCM Test Share With NC v28.0.14 to NC v27.1.11 # Controls when the action will run. on: @@ -29,7 +29,7 @@ jobs: sender: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] receiver: [ diff --git a/.github/workflows/share-with-nc-v28-nc-v28.yml b/.github/workflows/share-with-nc-v28-nc-v28.yml index 83af68f1..8c952c54 100644 --- a/.github/workflows/share-with-nc-v28-nc-v28.yml +++ b/.github/workflows/share-with-nc-v28-nc-v28.yml @@ -1,4 +1,4 @@ -name: OCM Test Share With NC v28.0.12 to NC v28.0.12 +name: OCM Test Share With NC v28.0.14 to NC v28.0.14 # Controls when the action will run. on: @@ -29,13 +29,13 @@ jobs: sender: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] receiver: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] diff --git a/.github/workflows/share-with-nc-v28-oc-v10.yml b/.github/workflows/share-with-nc-v28-oc-v10.yml index c16ba310..91d93b32 100644 --- a/.github/workflows/share-with-nc-v28-oc-v10.yml +++ b/.github/workflows/share-with-nc-v28-oc-v10.yml @@ -1,4 +1,4 @@ -name: OCM Test Share With NC v28.0.12 to OC v10.15.0 +name: OCM Test Share With NC v28.0.14 to OC v10.15.0 # Controls when the action will run. on: @@ -29,7 +29,7 @@ jobs: sender: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] receiver: [ diff --git a/.github/workflows/share-with-nc-v28-os-v1.yml b/.github/workflows/share-with-nc-v28-os-v1.yml index f8b027bd..2a923629 100644 --- a/.github/workflows/share-with-nc-v28-os-v1.yml +++ b/.github/workflows/share-with-nc-v28-os-v1.yml @@ -1,4 +1,4 @@ -name: OCM Test Share With NC v28.0.12 to OcmStub v1.0.0 +name: OCM Test Share With NC v28.0.14 to OcmStub v1.0.0 # Controls when the action will run. on: @@ -29,7 +29,7 @@ jobs: sender: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] receiver: [ diff --git a/.github/workflows/share-with-oc-v10-nc-v28.yml b/.github/workflows/share-with-oc-v10-nc-v28.yml index ea2fa8ec..81cd784c 100644 --- a/.github/workflows/share-with-oc-v10-nc-v28.yml +++ b/.github/workflows/share-with-oc-v10-nc-v28.yml @@ -1,4 +1,4 @@ -name: OCM Test Share With OC v10.15.0 to NC v28.0.12 +name: OCM Test Share With OC v10.15.0 to NC v28.0.14 # Controls when the action will run. on: @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: nextcloud, - version: v28.0.12 + version: v28.0.14 }, ] diff --git a/README.md b/README.md index 0794cdec..04cdf662 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ Some popular EFSS platforms include **Nextcloud** and **ownCloud**, which provid | **Repository** | **Tag** | **Branch** | **Upstream** | |----------------------------------|-----------------|---------------------------------------------------------------------------|------------------------------------------------------------------------------| -| pondersource/dev-stock-nextcloud | latest, v30.0.0 | [v30.0.0](https://github.com/nextcloud/server/releases/tag/v30.0.0) | [Official Nextcloud Server](https://github.com/nextcloud/server) | -| pondersource/dev-stock-nextcloud | v29.0.8 | [v29.0.8](https://github.com/nextcloud/server/releases/tag/v29.0.8) | [Official Nextcloud Server](https://github.com/nextcloud/server) | -| pondersource/dev-stock-nextcloud | v28.0.12 | [v28.0.12](https://github.com/nextcloud/server/releases/tag/v28.0.12) | [Official Nextcloud Server](https://github.com/nextcloud/server) | +| pondersource/dev-stock-nextcloud | latest, v30.0.2 | [v30.0.2](https://github.com/nextcloud/server/releases/tag/v30.0.2) | [Official Nextcloud Server](https://github.com/nextcloud/server) | +| pondersource/dev-stock-nextcloud | v29.0.10 | [v29.0.10](https://github.com/nextcloud/server/releases/tag/v29.0.10) | [Official Nextcloud Server](https://github.com/nextcloud/server) | +| pondersource/dev-stock-nextcloud | v28.0.14 | [v28.0.14](https://github.com/nextcloud/server/releases/tag/v28.0.14) | [Official Nextcloud Server](https://github.com/nextcloud/server) | | pondersource/dev-stock-nextcloud | v27.1.11 | [v27.1.11](https://github.com/nextcloud/server/releases/tag/v27.1.11) | [Official Nextcloud Server](https://github.com/nextcloud/server) | --- @@ -71,8 +71,8 @@ To pull the Docker images for EFSS, use the following commands: docker pull pondersource/dev-stock-nextcloud:latest # Pull a specific version of Nextcloud -docker pull pondersource/dev-stock-nextcloud:v30.0.0 -docker pull pondersource/dev-stock-nextcloud:v29.0.8 +docker pull pondersource/dev-stock-nextcloud:v30.0.2 +docker pull pondersource/dev-stock-nextcloud:v29.0.10 # Pull a specific version of ownCloud docker pull pondersource/dev-stock-owncloud:v10.15.0 @@ -206,19 +206,19 @@ To learn more about the **Open Cloud Mesh** standard, visit: [OCM-API](https://g ## Login Tests 🔐 Verifies authentication mechanisms for supported EFSS platforms, ensuring users can securely log in. -| Test Name | Nextcloud v27.1.11 | Nextcloud v28.0.12 | oCIS v5.0.9 | OcmStub v1.0.0 | ownCloud v10.15.0 | Seafile v11.0.5 | +| Test Name | Nextcloud v27.1.11 | Nextcloud v28.0.14 | oCIS v5.0.9 | OcmStub v1.0.0 | ownCloud v10.15.0 | Seafile v11.0.5 | |-----------|--------------------|--------------------|-------------|----------------|-------------------|-----------------| -| **Login** | [![NC v27.1.11](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-nextcloud-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-nextcloud-v27.yml) | [![NC v28.0.12](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-nextcloud-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-nextcloud-v28.yml) | [![oCIS v5.0.9](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-ocis-v5.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-ocis-v5.yml) | [![OcmStub v1.0](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-ocmstub-v1.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-ocmstub-v1.yml) | [![ownCloud v10.15.0](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-owncloud-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-owncloud-v10.yml) | [![Seafile v11.0.5](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-seafile-v11.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-seafile-v11.yml) | +| **Login** | [![NC v27.1.11](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-nextcloud-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-nextcloud-v27.yml) | [![NC v28.0.14](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-nextcloud-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-nextcloud-v28.yml) | [![oCIS v5.0.9](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-ocis-v5.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-ocis-v5.yml) | [![OcmStub v1.0](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-ocmstub-v1.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-ocmstub-v1.yml) | [![ownCloud v10.15.0](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-owncloud-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-owncloud-v10.yml) | [![Seafile v11.0.5](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/login-seafile-v11.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/login-seafile-v11.yml) | --- ## Share Link Tests 🔗 Tests the ability to create and manage public links for file sharing, and their integration into EFSS platforms. -| Sender (R) / Receiver (C) | Nextcloud v27.1.11 | Nextcloud v28.0.12 | ownCloud v10.15.0 | +| Sender (R) / Receiver (C) | Nextcloud v27.1.11 | Nextcloud v28.0.14 | ownCloud v10.15.0 | |---------------------------|--------------------|--------------------|-------------------| | **Nextcloud v27.1.11** | [![NC v27 ↔ NC v27](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v27-nc-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v27-nc-v27.yml) | [![NC v27 ↔ NC v28](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v27-nc-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v27-nc-v28.yml) | [![NC v27 ↔ OC v10](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v27-oc-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v27-oc-v10.yml) | -| **Nextcloud v28.0.12** | [![NC v28 ↔ NC v27](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v28-nc-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v28-nc-v27.yml) | [![NC v28 ↔ NC v28](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v28-nc-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v28-nc-v28.yml) | [![NC v28 ↔ OC v10](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v28-oc-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v28-oc-v10.yml) | +| **Nextcloud v28.0.14** | [![NC v28 ↔ NC v27](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v28-nc-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v28-nc-v27.yml) | [![NC v28 ↔ NC v28](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v28-nc-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v28-nc-v28.yml) | [![NC v28 ↔ OC v10](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-nc-v28-oc-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-nc-v28-oc-v10.yml) | | **ownCloud v10.15.0** | [![OC v10 ↔ NC v27](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-oc-v10-nc-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-oc-v10-nc-v27.yml) | [![OC v10 ↔ NC v28](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-oc-v10-nc-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-oc-v10-nc-v28.yml) | [![OC v10 ↔ OC v10](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-link-oc-v10-oc-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-link-oc-v10-oc-v10.yml) | --- @@ -226,10 +226,10 @@ Tests the ability to create and manage public links for file sharing, and their ## Share With Tests 🤝 Validates direct file sharing between users on different EFSS platforms, ensuring seamless collaboration. -| Sender (R) / Receiver (C) | Nextcloud v27.1.11 | Nextcloud v28.0.12 | OcmStub v1.0.0 | ownCloud v10.15.0 | Seafile v11.0.5 | +| Sender (R) / Receiver (C) | Nextcloud v27.1.11 | Nextcloud v28.0.14 | OcmStub v1.0.0 | ownCloud v10.15.0 | Seafile v11.0.5 | |---------------------------|--------------------|--------------------|----------------|-------------------|-----------------| | **Nextcloud v27.1.11** | [![NC ↔ NC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v27-nc-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v27-nc-v27.yml) | [![NC ↔ NC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v27-nc-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v27-nc-v28.yml) | [![NC ↔ OS](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v27-os-v1.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v27-os-v1.yml) | [![NC ↔ OC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v27-oc-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v27-oc-v10.yml) | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | -| **Nextcloud v28.0.12** | [![NC ↔ NC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v28-nc-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v28-nc-v27.yml) | [![NC ↔ NC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v28-nc-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v28-nc-v28.yml) | [![NC ↔ OS](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v28-os-v1.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v28-os-v1.yml) | [![NC ↔ OC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v28-oc-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v28-oc-v10.yml) | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | +| **Nextcloud v28.0.14** | [![NC ↔ NC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v28-nc-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v28-nc-v27.yml) | [![NC ↔ NC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v28-nc-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v28-nc-v28.yml) | [![NC ↔ OS](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v28-os-v1.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v28-os-v1.yml) | [![NC ↔ OC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-nc-v28-oc-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-nc-v28-oc-v10.yml) | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | | **OcmStub v1.0.0** | ![Possible](https://img.shields.io/badge/Possible-blue?style=flat-square) | ![Possible](https://img.shields.io/badge/Possible-blue?style=flat-square) | [![OS ↔ OS](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-os-v1-os-v1.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-os-v1-os-v1.yml) | ![Possible](https://img.shields.io/badge/Possible-blue?style=flat-square) | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | | **ownCloud v10.15.0** | [![OC ↔ NC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-oc-v10-nc-v27.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-oc-v10-nc-v27.yml) | [![OC ↔ NC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-oc-v10-nc-v28.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-oc-v10-nc-v28.yml) | [![OC ↔ OS](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-oc-v10-os-v1.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-oc-v10-os-v1.yml) | [![OC ↔ OC](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-oc-v10-oc-v10.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-oc-v10-oc-v10.yml) | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | | **Seafile v11.0.5** | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | ![Impossible](https://img.shields.io/badge/Impossible-orange?style=flat-square) | [![SF ↔ SF](https://img.shields.io/github/actions/workflow/status/pondersource/dev-stock/share-with-sf-v11-sf-v11.yml?branch=main&style=flat-square&label=)](https://github.com/pondersource/dev-stock/actions/workflows/share-with-sf-v11-sf-v11.yml) | @@ -298,7 +298,7 @@ To run specific tests, use the following command syntax: 4. `ocis` 7. **Platform 2 Version (Optional):** - - The specific version of Platform 2. For example: `v28.0.12`. + - The specific version of Platform 2. For example: `v28.0.14`. ## Example Usage 📘 @@ -320,10 +320,10 @@ Run a "login" test on a Seafile instance using version `v11.0.5`, in development ``` ### Running a share-link Test: -Run a "share-link" test between ownCloud and Nextcloud instances, using versions `v10.15.0` and `v29.0.8`, respectively, in CI mode with Firefox: +Run a "share-link" test between ownCloud and Nextcloud instances, using versions `v10.15.0` and `v29.0.10`, respectively, in CI mode with Firefox: ```bash -./dev/ocm-test-suite.sh share-link owncloud v10.15.0 ci firefox nextcloud v29.0.8 +./dev/ocm-test-suite.sh share-link owncloud v10.15.0 ci firefox nextcloud v29.0.10 ``` ## Notes 📝 diff --git a/dev/ocm-nextcloud.sh b/dev/ocm-nextcloud.sh index ebdafb40..e03df487 100755 --- a/dev/ocm-nextcloud.sh +++ b/dev/ocm-nextcloud.sh @@ -112,8 +112,8 @@ docker network inspect testnet >/dev/null 2>&1 || docker network create testnet # username: username for sign in into efss. # password: password for sign in into efss. # Nextclouds. -createEfss nextcloud 1 einstein relativity ocm-nextcloud1 nextcloud.sh v30.0.0 -createEfss nextcloud 2 michiel dejong ocm-nextcloud2 nextcloud.sh v30.0.0 +createEfss nextcloud 1 einstein relativity ocm-nextcloud1 nextcloud.sh v30.0.2 +createEfss nextcloud 2 michiel dejong ocm-nextcloud2 nextcloud.sh v30.0.2 ############### ### Firefox ### diff --git a/dev/ocm-test-suite/invite-link/nextcloud-ocis.sh b/dev/ocm-test-suite/invite-link/nextcloud-ocis.sh index a0b6091f..f80c31a6 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-ocis.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-ocis.sh @@ -20,7 +20,7 @@ export ENV_ROOT=${ENV_ROOT} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} # oCIS version: diff --git a/dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh b/dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh index ae2676b9..d81c856c 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh @@ -20,7 +20,7 @@ export ENV_ROOT=${ENV_ROOT} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} # owncloud version: diff --git a/dev/ocm-test-suite/invite-link/ocis-nextcloud.sh b/dev/ocm-test-suite/invite-link/ocis-nextcloud.sh index 6bd12fac..b3c7b20f 100755 --- a/dev/ocm-test-suite/invite-link/ocis-nextcloud.sh +++ b/dev/ocm-test-suite/invite-link/ocis-nextcloud.sh @@ -24,7 +24,7 @@ EFSS_PLATFORM_1_VERSION=${1:-"5.0.9"} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_2_VERSION=${2:-"v27.1.11"} # script mode: dev, ci. default is dev. diff --git a/dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh b/dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh index c43c0723..577b8e3c 100755 --- a/dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh +++ b/dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh @@ -24,8 +24,8 @@ EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} # nextcloud version: # - v27.1.11 -# - v28.0.12 -EFSS_PLATFORM_2_VERSION=${2:-"v28.0.12"} +# - v28.0.14 +EFSS_PLATFORM_2_VERSION=${2:-"v28.0.14"} # script mode: dev, ci. default is dev. SCRIPT_MODE=${3:-"dev"} diff --git a/dev/ocm-test-suite/login/nextcloud.sh b/dev/ocm-test-suite/login/nextcloud.sh index f047dde5..0af86675 100755 --- a/dev/ocm-test-suite/login/nextcloud.sh +++ b/dev/ocm-test-suite/login/nextcloud.sh @@ -20,7 +20,7 @@ export ENV_ROOT=${ENV_ROOT} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_VERSION=${1:-"v27.1.11"} # script mode: dev, ci. default is dev. diff --git a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh index ea2701c7..ddc2c56f 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh @@ -20,12 +20,12 @@ export ENV_ROOT=${ENV_ROOT} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_2_VERSION=${2:-"v27.1.11"} # script mode: dev, ci. default is dev. diff --git a/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh b/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh index 53115478..91518da9 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh @@ -20,7 +20,7 @@ export ENV_ROOT=${ENV_ROOT} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} # owncloud version: diff --git a/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh b/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh index ecd7d9b7..735d31c8 100755 --- a/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh @@ -24,8 +24,8 @@ EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} # nextcloud version: # - v27.1.11 -# - v28.0.12 -EFSS_PLATFORM_2_VERSION=${2:-"v28.0.12"} +# - v28.0.14 +EFSS_PLATFORM_2_VERSION=${2:-"v28.0.14"} # script mode: dev, ci. default is dev. SCRIPT_MODE=${3:-"dev"} diff --git a/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh index 378908a1..609ce4aa 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh @@ -20,7 +20,7 @@ export ENV_ROOT=${ENV_ROOT} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} # ocmstub version: diff --git a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh index 14a28409..78656ec9 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh @@ -20,7 +20,7 @@ export ENV_ROOT=${ENV_ROOT} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} # owncloud version: diff --git a/dev/ocm-test-suite/share-with/nextcloud-seafile.sh b/dev/ocm-test-suite/share-with/nextcloud-seafile.sh index f0271bd5..5891857b 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-seafile.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-seafile.sh @@ -20,7 +20,7 @@ export ENV_ROOT=${ENV_ROOT} # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_1_VERSION=${1:-"11.0.5"} # seafile version: diff --git a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh index 280a98bd..eff36fff 100755 --- a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh @@ -24,8 +24,8 @@ EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} # nextcloud version: # - v27.1.11 -# - v28.0.12 -EFSS_PLATFORM_2_VERSION=${2:-"v28.0.12"} +# - v28.0.14 +EFSS_PLATFORM_2_VERSION=${2:-"v28.0.14"} # script mode: dev, ci. default is dev. SCRIPT_MODE=${3:-"dev"} diff --git a/docker/build/ocm-test-suite.sh b/docker/build/ocm-test-suite.sh index f8977b5c..a1327781 100755 --- a/docker/build/ocm-test-suite.sh +++ b/docker/build/ocm-test-suite.sh @@ -30,16 +30,16 @@ echo Building pondersource/dev-stock-php-base docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v30.0.0" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.0 --tag pondersource/dev-stock-nextcloud:latest . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v30.0.2" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.2 --tag pondersource/dev-stock-nextcloud:latest . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v29.0.8" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.8 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v29.0.10" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.10 . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v28.0.12" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.12 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v28.0.14" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.14 . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . echo Building pondersource/dev-stock-nextcloud-sciencemesh docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-sciencemesh.Dockerfile --tag pondersource/dev-stock-nextcloud-sciencemesh:latest . diff --git a/docker/build/php-base.sh b/docker/build/php-base.sh index 902c1d48..68e75113 100755 --- a/docker/build/php-base.sh +++ b/docker/build/php-base.sh @@ -36,4 +36,4 @@ docker build --build-arg CACHEBUST="default" \ . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v28.0.12" --file ./dockerfiles/nextcloud-base.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.12 --tag pondersource/dev-stock-nextcloud:latest . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v28.0.14" --file ./dockerfiles/nextcloud-base.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.14 --tag pondersource/dev-stock-nextcloud:latest . diff --git a/docker/build/sciencemesh.sh b/docker/build/sciencemesh.sh index 59d2b533..6e2b0dd9 100755 --- a/docker/build/sciencemesh.sh +++ b/docker/build/sciencemesh.sh @@ -30,16 +30,16 @@ echo Building pondersource/dev-stock-php-base docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v30.0.0" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.0 --tag pondersource/dev-stock-nextcloud:latest . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v30.0.2" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.2 --tag pondersource/dev-stock-nextcloud:latest . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v29.0.8" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.8 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v29.0.10" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.10 . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v28.0.12" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.12 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v28.0.14" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.14 . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . echo Building pondersource/dev-stock-nextcloud-sciencemesh docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-sciencemesh.Dockerfile --tag pondersource/dev-stock-nextcloud-sciencemesh:latest . diff --git a/docker/build/solid.sh b/docker/build/solid.sh index 1f734a87..76630a18 100755 --- a/docker/build/solid.sh +++ b/docker/build/solid.sh @@ -27,19 +27,19 @@ echo Building pondersource/dev-stock-php-base docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v30.0.0" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.0 --tag pondersource/dev-stock-nextcloud:latest . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v30.0.2" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.2 --tag pondersource/dev-stock-nextcloud:latest . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v30.0.0" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.0 --tag pondersource/dev-stock-nextcloud:latest . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v30.0.2" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.2 --tag pondersource/dev-stock-nextcloud:latest . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v29.0.8" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.8 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v29.0.10" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.10 . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v28.0.12" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.12 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v28.0.14" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.14 . echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . +docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . echo Building pondersource/dev-stock-nextcloud-solid docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-solid.Dockerfile --tag pondersource/dev-stock-nextcloud-solid:latest . diff --git a/docker/pull/all.sh b/docker/pull/all.sh index 2277ff20..4e5b6223 100755 --- a/docker/pull/all.sh +++ b/docker/pull/all.sh @@ -21,9 +21,9 @@ docker pull pondersource/dev-stock-ocmstub:latest docker pull pondersource/dev-stock-revad:latest docker pull pondersource/dev-stock-php-base:latest docker pull pondersource/dev-stock-nextcloud:latest -docker pull pondersource/dev-stock-nextcloud:v30.0.0 +docker pull pondersource/dev-stock-nextcloud:v30.0.2 docker pull pondersource/dev-stock-nextcloud:v39.0.8 -docker pull pondersource/dev-stock-nextcloud:v28.0.12 +docker pull pondersource/dev-stock-nextcloud:v28.0.14 docker pull pondersource/dev-stock-nextcloud:v27.1.11 # docker pull pondersource/dev-stock-nextcloud-sunet:latest # docker pull pondersource/dev-stock-simple-saml-php:latest diff --git a/docker/pull/ocm-test-suite.sh b/docker/pull/ocm-test-suite.sh index 3a9722b9..17589ad2 100755 --- a/docker/pull/ocm-test-suite.sh +++ b/docker/pull/ocm-test-suite.sh @@ -14,9 +14,9 @@ docker pull seafileltd/seafile-mc:11.0.5 # dev-stock images. docker pull pondersource/dev-stock-revad:latest docker pull pondersource/dev-stock-ocmstub:latest -docker pull pondersource/dev-stock-nextcloud:v30.0.0 +docker pull pondersource/dev-stock-nextcloud:v30.0.2 docker pull pondersource/dev-stock-nextcloud:v39.0.8 -docker pull pondersource/dev-stock-nextcloud:v28.0.12 +docker pull pondersource/dev-stock-nextcloud:v28.0.14 docker pull pondersource/dev-stock-nextcloud:v27.1.11 docker pull pondersource/dev-stock-nextcloud-sciencemesh:latest docker pull pondersource/dev-stock-owncloud-sciencemesh:latest diff --git a/docker/pull/ocm-test-suite/nextcloud.sh b/docker/pull/ocm-test-suite/nextcloud.sh index 4349be08..f3c76af0 100755 --- a/docker/pull/ocm-test-suite/nextcloud.sh +++ b/docker/pull/ocm-test-suite/nextcloud.sh @@ -5,7 +5,7 @@ set -e # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_VERSION=${1:-"v27.1.11"} # 3rd party images. diff --git a/docker/pull/ocm-test-suite/ocmstub.sh b/docker/pull/ocm-test-suite/ocmstub.sh index e6521a50..6b085b7f 100755 --- a/docker/pull/ocm-test-suite/ocmstub.sh +++ b/docker/pull/ocm-test-suite/ocmstub.sh @@ -5,7 +5,7 @@ set -e # nextcloud version: # - v27.1.11 -# - v28.0.12 +# - v28.0.14 EFSS_PLATFORM_VERSION=${1:-"1.0"} # dev-stock images. diff --git a/docker/pull/sciencemesh.sh b/docker/pull/sciencemesh.sh index 854417bf..cdec8439 100755 --- a/docker/pull/sciencemesh.sh +++ b/docker/pull/sciencemesh.sh @@ -4,9 +4,9 @@ set -e docker pull pondersource/dev-stock-revad:latest -docker pull pondersource/dev-stock-nextcloud:v30.0.0 +docker pull pondersource/dev-stock-nextcloud:v30.0.2 docker pull pondersource/dev-stock-nextcloud:v39.0.8 -docker pull pondersource/dev-stock-nextcloud:v28.0.12 +docker pull pondersource/dev-stock-nextcloud:v28.0.14 docker pull pondersource/dev-stock-nextcloud:v27.1.11 docker pull pondersource/dev-stock-nextcloud-sciencemesh:latest docker pull pondersource/dev-stock-owncloud-sciencemesh:latest diff --git a/docker/push/all.sh b/docker/push/all.sh index 85928731..0a279fd0 100755 --- a/docker/push/all.sh +++ b/docker/push/all.sh @@ -11,9 +11,9 @@ docker push pondersource/dev-stock-ocmstub:v1.0.0 docker push pondersource/dev-stock-revad:latest docker push pondersource/dev-stock-php-base:latest docker push pondersource/dev-stock-nextcloud:latest -docker push pondersource/dev-stock-nextcloud:v30.0.0 -docker push pondersource/dev-stock-nextcloud:v29.0.8 -docker push pondersource/dev-stock-nextcloud:v28.0.12 +docker push pondersource/dev-stock-nextcloud:v30.0.2 +docker push pondersource/dev-stock-nextcloud:v29.0.10 +docker push pondersource/dev-stock-nextcloud:v28.0.14 docker push pondersource/dev-stock-nextcloud:v27.1.11 # docker push pondersource/dev-stock-nextcloud-sunet # docker push pondersource/dev-stock-simple-saml-php From 9aa937a6bcd8dd014a8a535c2d85729b76264d69 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 14 Dec 2024 11:25:16 +0000 Subject: [PATCH 021/184] remove: unused dockerfiles --- docker/dockerfiles/php-base-7.4.Dockerfile | 9 --------- docker/dockerfiles/php-base-8.3.Dockerfile | 8 -------- 2 files changed, 17 deletions(-) delete mode 100644 docker/dockerfiles/php-base-7.4.Dockerfile delete mode 100644 docker/dockerfiles/php-base-8.3.Dockerfile diff --git a/docker/dockerfiles/php-base-7.4.Dockerfile b/docker/dockerfiles/php-base-7.4.Dockerfile deleted file mode 100644 index c1f54ffd..00000000 --- a/docker/dockerfiles/php-base-7.4.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM php:7.4.33-zts-alpine3.16 - -# Setup apache and php -RUN apk --no-cache --update add \ - git \ - curl \ - apache2 \ - apache2-ssl - diff --git a/docker/dockerfiles/php-base-8.3.Dockerfile b/docker/dockerfiles/php-base-8.3.Dockerfile deleted file mode 100644 index 5ffe8f7a..00000000 --- a/docker/dockerfiles/php-base-8.3.Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM php:8.3.13-zts-alpine3.20@sha256:e53b96684f35685cd2a2f2f5326187e9130cc56958757121b2e52149a1eebaf4 - -# Setup apache and php -RUN apk --no-cache --update add \ - git \ - curl \ - apache2 \ - apache2-ssl From c358d412e545d4906bbb47c577acd13e1bdd7db2 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 14 Dec 2024 11:25:41 +0000 Subject: [PATCH 022/184] add: new dockerfile for nextcloud --- docker/dockerfiles/nextcloud-base.Dockerfile | 182 +++++++++++++++---- docker/dockerfiles/nextcloud.Dockerfile | 52 ++---- 2 files changed, 171 insertions(+), 63 deletions(-) diff --git a/docker/dockerfiles/nextcloud-base.Dockerfile b/docker/dockerfiles/nextcloud-base.Dockerfile index 803d960c..6800f6ac 100644 --- a/docker/dockerfiles/nextcloud-base.Dockerfile +++ b/docker/dockerfiles/nextcloud-base.Dockerfile @@ -1,47 +1,167 @@ -FROM pondersource/php-base:8.3 +FROM php:8.2-apache-bookworm@sha256:b8d8c9d7882fdea9d2ef5b3829bf9e34fb368f833c52f13ea64706df27cb6561 # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys LABEL org.opencontainers.image.licenses=MIT -LABEL org.opencontainers.image.title="PonderSource Nextcloud Image" +LABEL org.opencontainers.image.title="PonderSource Nextcloud Base Image" LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" -# remove html directory and recreate it with correct permissions -RUN rm -rf /var/www/html && mkdir /var/www/html -RUN chown -R www-data:www-data /var/www/html -RUN chmod -R 775 /var/www/html +# entrypoint.sh and cron.sh dependencies +RUN set -ex; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + git \ + vim \ + curl\ + bzip2 \ + rsync \ + iproute2 \ + busybox-static \ + libldap-common \ + ca-certificates \ + libmagickcore-6.q16-6-extra \ + ; \ + rm -rf /var/lib/apt/lists/*; \ + \ + mkdir -p /var/spool/cron/crontabs; \ + echo '*/5 * * * * php -f /var/www/html/cron.php' > /var/spool/cron/crontabs/www-data -WORKDIR /var/www/html +# install the PHP extensions we need +# see https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html +ENV PHP_MEMORY_LIMIT 512M +ENV PHP_UPLOAD_LIMIT 512M +RUN set -ex; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libcurl4-openssl-dev \ + libevent-dev \ + libfreetype6-dev \ + libgmp-dev \ + libicu-dev \ + libjpeg-dev \ + libldap2-dev \ + libmagickwand-dev \ + libmcrypt-dev \ + libmemcached-dev \ + libpng-dev \ + libpq-dev \ + libwebp-dev \ + libxml2-dev \ + libzip-dev \ + ; \ + \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + docker-php-ext-configure ftp --with-openssl-dir=/usr; \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \ + docker-php-ext-install -j "$(nproc)" \ + bcmath \ + exif \ + ftp \ + gd \ + gmp \ + intl \ + ldap \ + opcache \ + pcntl \ + pdo_mysql \ + pdo_pgsql \ + sysvsem \ + zip \ + ; \ + \ + # pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu-5.1.24; \ + pecl install imagick-3.7.0; \ + pecl install memcached-3.3.0; \ + pecl install redis-6.1.0; \ + \ + docker-php-ext-enable \ + apcu \ + imagick \ + memcached \ + redis \ + ; \ + rm -r /tmp/pear; \ + \ + # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \ + | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); print so }' \ + | sort -u \ + | xargs -r dpkg-query --search \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/* -USER www-data +# set recommended PHP.ini settings +# see https://docs.nextcloud.com/server/latest/admin_manual/installation/server_tuning.html#enable-php-opcache +RUN { \ + echo 'opcache.enable=1'; \ + echo 'opcache.interned_strings_buffer=32'; \ + echo 'opcache.max_accelerated_files=10000'; \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.save_comments=1'; \ + echo 'opcache.revalidate_freq=60'; \ + echo 'opcache.jit=1255'; \ + echo 'opcache.jit_buffer_size=128M'; \ + } > "${PHP_INI_DIR}/conf.d/opcache-recommended.ini"; \ + \ + echo 'apc.enable_cli=1' >> "${PHP_INI_DIR}/conf.d/docker-php-ext-apcu.ini"; \ + \ + { \ + echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \ + echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \ + echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \ + } > "${PHP_INI_DIR}/conf.d/nextcloud.ini"; \ + \ + mkdir /var/www/data; \ + mkdir -p /docker-entrypoint-hooks.d/pre-installation \ + /docker-entrypoint-hooks.d/post-installation \ + /docker-entrypoint-hooks.d/pre-upgrade \ + /docker-entrypoint-hooks.d/post-upgrade \ + /docker-entrypoint-hooks.d/before-starting; \ + chown -R www-data:root /var/www; \ + chmod -R g=u /var/www -ARG REPO_NEXTCLOUD=https://github.com/nextcloud/server -ARG BRANCH_NEXTCLOUD=v28.0.12 -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . -# $RANDOM returns random number each time. -ARG CACHEBUST="default" -RUN git clone \ - --depth 1 \ - --recursive \ - --shallow-submodules \ - --branch ${BRANCH_NEXTCLOUD} \ - ${REPO_NEXTCLOUD} \ - . +VOLUME /var/www/html -USER root +COPY ./tls/certificates/* /tls/ +COPY ./tls/certificate-authority/* /tls/ +RUN ln --symbolic --force /tls/*.crt /usr/local/share/ca-certificates; \ + update-ca-certificates -ENV PHP_MEMORY_LIMIT="512M" +COPY ./configs/nextcloud/apache.conf /etc/apache2/sites-enabled/000-default.conf + +RUN a2enmod headers rewrite remoteip ssl; \ + { \ + echo 'RemoteIPHeader X-Real-IP'; \ + echo 'RemoteIPInternalProxy 10.0.0.0/8'; \ + echo 'RemoteIPInternalProxy 172.16.0.0/12'; \ + echo 'RemoteIPInternalProxy 192.168.0.0/16'; \ + } > /etc/apache2/conf-available/remoteip.conf; \ + a2enconf remoteip; \ + chown -R www-data:root /var/log/apache2; \ + chmod -R g=u /var/log/apache2 + +# set apache config LimitRequestBody +ENV APACHE_BODY_LIMIT 1073741824 +RUN { \ + echo 'LimitRequestBody ${APACHE_BODY_LIMIT}'; \ + } > /etc/apache2/conf-available/apache-limits.conf; \ + a2enconf apache-limits RUN curl --silent --show-error https://getcomposer.org/installer -o /root/composer-setup.php RUN php /root/composer-setup.php --install-dir=/usr/local/bin --filename=composer -USER www-data -# this file can be overrided in docker run or docker compose.yaml. -# example: docker run --volume new-init.sh:/init.sh:ro -COPY ./scripts/init/nextcloud.sh /init.sh -RUN mkdir -p data; touch data/nextcloud.log - -USER root -CMD /usr/sbin/httpd -DFOREGROUND & tail -f /var/log/apache2/access.log & tail -f /var/log/apache2/error.log & tail -f data/nextcloud.log +ENTRYPOINT ["/entrypoint.sh"] +CMD apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow /var/www/html/data/nextcloud.log diff --git a/docker/dockerfiles/nextcloud.Dockerfile b/docker/dockerfiles/nextcloud.Dockerfile index e038f672..f3f9d1f1 100644 --- a/docker/dockerfiles/nextcloud.Dockerfile +++ b/docker/dockerfiles/nextcloud.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-php-base:latest +FROM pondersource/dev-stock-nextcloud-base:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys @@ -7,39 +7,27 @@ LABEL org.opencontainers.image.title="PonderSource Nextcloud Image" LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" -RUN rm --recursive --force /var/www/html -USER www-data +ARG NEXTCLOUD_REPO=https://github.com/nextcloud/server +ARG NEXTCLOUD_BRANCH=v30.0.2 -ARG REPO_NEXTCLOUD=https://github.com/nextcloud/server -ARG BRANCH_NEXTCLOUD=v30.0.0 # CACHEBUST forces docker to clone fresh source codes from git. # example: docker build -t your-image --build-arg CACHEBUST="default" . # $RANDOM returns random number each time. ARG CACHEBUST="default" -RUN git clone \ - --depth 1 \ - --recursive \ - --shallow-submodules \ - --branch ${BRANCH_NEXTCLOUD} \ - ${REPO_NEXTCLOUD} \ - html - -USER root -WORKDIR /var/www/html - -# switch php version for Nextloud. -RUN switch-php.sh 8.2 - -ENV PHP_MEMORY_LIMIT="512M" - -RUN curl --silent --show-error https://getcomposer.org/installer -o /root/composer-setup.php -RUN php /root/composer-setup.php --install-dir=/usr/local/bin --filename=composer - -USER www-data -# this file can be overrided in docker run or docker compose.yaml. -# example: docker run --volume new-init.sh:/init.sh:ro -COPY ./scripts/init/nextcloud.sh /init.sh -RUN mkdir -p data; touch data/nextcloud.log - -USER root -CMD /usr/sbin/apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow data/nextcloud.log +RUN set -ex; \ + cd /usr/src/; \ + git clone \ + --depth 1 \ + --recursive \ + --shallow-submodules \ + --branch ${NEXTCLOUD_BRANCH} \ + ${NEXTCLOUD_REPO} \ + nextcloud; \ + rm -rf /usr/src/nextcloud/.git; \ + mkdir -p /usr/src/nextcloud/data; \ + mkdir -p /usr/src/nextcloud/custom_apps; \ + chmod +x /usr/src/nextcloud/occ + +COPY ./scripts/nextcloud/*.sh / +COPY ./scripts/nextcloud/upgrade.exclude / +COPY ./configs/nextcloud/* /usr/src/nextcloud/config/ From 55942ce5a14e6740e032ae49cf7e5e645cad3bcb Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 14 Dec 2024 17:25:52 +0000 Subject: [PATCH 023/184] fix: bug related to entrypoint and cmd being out of order --- docker/dockerfiles/nextcloud-base.Dockerfile | 5 +---- docker/dockerfiles/nextcloud.Dockerfile | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/dockerfiles/nextcloud-base.Dockerfile b/docker/dockerfiles/nextcloud-base.Dockerfile index 6800f6ac..05650616 100644 --- a/docker/dockerfiles/nextcloud-base.Dockerfile +++ b/docker/dockerfiles/nextcloud-base.Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.2-apache-bookworm@sha256:b8d8c9d7882fdea9d2ef5b3829bf9e34fb368f833c52f13ea64706df27cb6561 +FROM php:8.2.26-apache-bookworm@sha256:b8d8c9d7882fdea9d2ef5b3829bf9e34fb368f833c52f13ea64706df27cb6561 # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys @@ -162,6 +162,3 @@ RUN { \ RUN curl --silent --show-error https://getcomposer.org/installer -o /root/composer-setup.php RUN php /root/composer-setup.php --install-dir=/usr/local/bin --filename=composer - -ENTRYPOINT ["/entrypoint.sh"] -CMD apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow /var/www/html/data/nextcloud.log diff --git a/docker/dockerfiles/nextcloud.Dockerfile b/docker/dockerfiles/nextcloud.Dockerfile index f3f9d1f1..816b15d1 100644 --- a/docker/dockerfiles/nextcloud.Dockerfile +++ b/docker/dockerfiles/nextcloud.Dockerfile @@ -31,3 +31,6 @@ RUN set -ex; \ COPY ./scripts/nextcloud/*.sh / COPY ./scripts/nextcloud/upgrade.exclude / COPY ./configs/nextcloud/* /usr/src/nextcloud/config/ + +ENTRYPOINT ["/entrypoint.sh"] +CMD apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow /var/www/html/data/nextcloud.log From a0fd7a2bac7a7f2b46480c4fb0fd1fa2678dd0e5 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 16 Dec 2024 20:47:56 +0000 Subject: [PATCH 024/184] [no ci] update: nektos act gh action runner --- .gitpod.Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index a93a9568..bca63182 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -11,6 +11,5 @@ ENV PATH="/dev-stock/bin:${PATH}" # initialize act rc with medium images. RUN echo "-P ubuntu-latest=catthehacker/ubuntu:act-latest" >> /home/gitpod/.actrc +RUN echo "-P ubuntu-24.04=catthehacker/ubuntu:act-24.04" >> /home/gitpod/.actrc RUN echo "-P ubuntu-22.04=catthehacker/ubuntu:act-22.04" >> /home/gitpod/.actrc -RUN echo "-P ubuntu-20.04=catthehacker/ubuntu:act-20.04" >> /home/gitpod/.actrc -RUN echo "-P ubuntu-18.04=catthehacker/ubuntu:act-18.04" >> /home/gitpod/.actrc From b0ab545931ccd51593f2dac9a9426024ea6d64a8 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Mon, 16 Dec 2024 20:48:37 +0000 Subject: [PATCH 025/184] delete: sunet --- docker/sunet/000-default.conf | 38 ----- docker/sunet/39411.diff | 21 --- docker/sunet/40235.diff | 154 ------------------ .../512b0a7c52640c9da8905e52fc906e72.patch | 14 -- docker/sunet/cron.sh | 4 - docker/sunet/entrypoint.sh | 13 -- docker/sunet/nextcloud-rds.tar.gz | Bin 780560 -> 0 bytes docker/sunet/workflowengine-workflowengine.js | 3 - .../workflowengine-workflowengine.js.map | 1 - 9 files changed, 248 deletions(-) delete mode 100644 docker/sunet/000-default.conf delete mode 100644 docker/sunet/39411.diff delete mode 100644 docker/sunet/40235.diff delete mode 100644 docker/sunet/512b0a7c52640c9da8905e52fc906e72.patch delete mode 100755 docker/sunet/cron.sh delete mode 100755 docker/sunet/entrypoint.sh delete mode 100644 docker/sunet/nextcloud-rds.tar.gz delete mode 100644 docker/sunet/workflowengine-workflowengine.js delete mode 100644 docker/sunet/workflowengine-workflowengine.js.map diff --git a/docker/sunet/000-default.conf b/docker/sunet/000-default.conf deleted file mode 100644 index d7e45d31..00000000 --- a/docker/sunet/000-default.conf +++ /dev/null @@ -1,38 +0,0 @@ -# This file is from the docker image - - ServerAdmin webmaster@localhost - DocumentRoot /var/www/html - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - Require all granted - AllowOverride All - Options FollowSymLinks MultiViews - - - Dav off - - - - - ServerAdmin webmaster@localhost - DocumentRoot /var/www/html - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - SSLEngine On - SSLCertificateFile "/tls/server.cert" - SSLCertificateKeyFile "/tls/server.key" - - Require all granted - AllowOverride All - Options FollowSymLinks MultiViews - - - Dav off - - - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/docker/sunet/39411.diff b/docker/sunet/39411.diff deleted file mode 100644 index 4e03aec2..00000000 --- a/docker/sunet/39411.diff +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php -index 7e115cf9b422..3e71d0787b31 100644 ---- a/lib/private/Authentication/TwoFactorAuth/Manager.php -+++ b/lib/private/Authentication/TwoFactorAuth/Manager.php -@@ -52,6 +52,7 @@ - class Manager { - public const SESSION_UID_KEY = 'two_factor_auth_uid'; - public const SESSION_UID_DONE = 'two_factor_auth_passed'; -+ public const SESSION_UID_CONFIGURING = 'two_factor_auth_configuring'; - public const REMEMBER_LOGIN = 'two_factor_remember_login'; - public const BACKUP_CODES_PROVIDER_ID = 'backup_codes'; - -@@ -359,7 +360,7 @@ public function needsSecondFactor(IUser $user = null): bool { - $tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa'); - - if (!\in_array((string) $tokenId, $tokensNeeding2FA, true)) { -- $this->session->set(self::SESSION_UID_DONE, $user->getUID()); -+ $this->session->set(self::SESSION_UID_CONFIGURING, $user->getUID()); - return false; - } - } catch (InvalidTokenException|SessionNotAvailableException $e) { diff --git a/docker/sunet/40235.diff b/docker/sunet/40235.diff deleted file mode 100644 index 64a2323a..00000000 --- a/docker/sunet/40235.diff +++ /dev/null @@ -1,154 +0,0 @@ ---- a/apps/workflowengine/composer/composer/autoload_classmap.php -+++ b/apps/workflowengine/composer/composer/autoload_classmap.php -@@ -19,6 +19,7 @@ return array( - 'OCA\\WorkflowEngine\\Check\\RequestURL' => $baseDir . '/../lib/Check/RequestURL.php', - 'OCA\\WorkflowEngine\\Check\\RequestUserAgent' => $baseDir . '/../lib/Check/RequestUserAgent.php', - 'OCA\\WorkflowEngine\\Check\\TFileCheck' => $baseDir . '/../lib/Check/TFileCheck.php', -+ 'OCA\\WorkflowEngine\\Check\\MfaVerified' => $baseDir . '/../lib/Check/MfaVerified.php', - 'OCA\\WorkflowEngine\\Check\\UserGroupMembership' => $baseDir . '/../lib/Check/UserGroupMembership.php', - 'OCA\\WorkflowEngine\\Command\\Index' => $baseDir . '/../lib/Command/Index.php', - 'OCA\\WorkflowEngine\\Controller\\AWorkflowController' => $baseDir . '/../lib/Controller/AWorkflowController.php', ---- a/apps/workflowengine/composer/composer/autoload_static.php -+++ b/apps/workflowengine/composer/composer/autoload_static.php -@@ -35,6 +35,7 @@ class ComposerStaticInitWorkflowEngine - 'OCA\\WorkflowEngine\\Check\\RequestUserAgent' => __DIR__ . '/..' . '/../lib/Check/RequestUserAgent.php', - 'OCA\\WorkflowEngine\\Check\\TFileCheck' => __DIR__ . '/..' . '/../lib/Check/TFileCheck.php', - 'OCA\\WorkflowEngine\\Check\\UserGroupMembership' => __DIR__ . '/..' . '/../lib/Check/UserGroupMembership.php', -+ 'OCA\\WorkflowEngine\\Check\\MfaVerified' => __DIR__ . '/..' . '/../lib/Check/MfaVerified.php', - 'OCA\\WorkflowEngine\\Command\\Index' => __DIR__ . '/..' . '/../lib/Command/Index.php', - 'OCA\\WorkflowEngine\\Controller\\AWorkflowController' => __DIR__ . '/..' . '/../lib/Controller/AWorkflowController.php', - 'OCA\\WorkflowEngine\\Controller\\GlobalWorkflowsController' => __DIR__ . '/..' . '/../lib/Controller/GlobalWorkflowsController.php', ---- /dev/null -+++ b/apps/workflowengine/lib/Check/MfaVerified.php -@@ -0,0 +1,90 @@ -+ -+ * -+ * @author Arthur Schiwon -+ * @author Christoph Wurst -+ * @author Joas Schilling -+ * @author Julius Härtl -+ * @author Richard Steinmetz -+ * -+ * @license GNU AGPL version 3 or any later version -+ * -+ * This program is free software: you can redistribute it and/or modify -+ * it under the terms of the GNU Affero General Public License as -+ * published by the Free Software Foundation, either version 3 of the -+ * License, or (at your option) any later version. -+ * -+ * This program is distributed in the hope that it will be useful, -+ * but WITHOUT ANY WARRANTY; without even the implied warranty of -+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ * GNU Affero General Public License for more details. -+ * -+ * You should have received a copy of the GNU Affero General Public License -+ * along with this program. If not, see . -+ * -+ */ -+namespace OCA\WorkflowEngine\Check; -+ -+use OCP\IL10N; -+use OCP\WorkflowEngine\ICheck; -+use OCP\ISession; -+ -+class MfaVerified implements ICheck{ -+ protected IL10N $l; -+ protected ISession $session; -+ -+ /** -+ * @param IL10N $l -+ * @param ISession $session -+ */ -+ public function __construct(IL10N $l, ISession $session) { -+ $this->l = $l; -+ $this->session = $session; -+ } -+ -+ /** -+ * @param string $operator -+ * @param string $value -+ * @return bool -+ */ -+ public function executeCheck($operator, $value): bool { -+ $mfaVerified = '0'; -+ if (!empty($this->session->get('globalScale.userData'))) { -+ $attr = $this->session->get('globalScale.userData')["userData"]; -+ $mfaVerified = $attr["mfaVerified"]; -+ } -+ if (!empty($this->session->get('user_saml.samlUserData'))) { -+ $attr = $this->session->get('user_saml.samlUserData'); -+ $mfaVerified = $attr["mfa_verified"][0]; -+ } -+ if (!empty($this->session->get("two_factor_auth_passed"))){ -+ $mfaVerified = '1'; -+ } -+ -+ if ($operator === 'is') { -+ return $mfaVerified === '1'; // checking whether the current user is MFA-verified -+ } else { -+ return $mfaVerified !== '1'; // checking whether the current user is not MFA-verified -+ } -+ } -+ -+ /** -+ * @param string $operator -+ * @param string $value -+ * @throws \UnexpectedValueException -+ */ -+ public function validateCheck($operator, $value): void { -+ if (!in_array($operator, ['is', '!is'])) { -+ throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1); -+ } -+ } -+ -+ public function supportedEntities(): array { -+ return []; -+ } -+ -+ public function isAvailableForScope(int $scope): bool { -+ return true; -+ } -+} -\ No newline at end of file ---- a/apps/workflowengine/lib/Manager.php -+++ b/apps/workflowengine/lib/Manager.php -@@ -36,6 +36,7 @@ use OCA\WorkflowEngine\Check\FileMimeType; - use OCA\WorkflowEngine\Check\FileName; - use OCA\WorkflowEngine\Check\FileSize; - use OCA\WorkflowEngine\Check\FileSystemTags; -+use OCA\WorkflowEngine\Check\MfaVerified; - use OCA\WorkflowEngine\Check\RequestRemoteAddress; - use OCA\WorkflowEngine\Check\RequestTime; - use OCA\WorkflowEngine\Check\RequestURL; -@@ -486,6 +487,13 @@ class Manager implements IManager { - return $result; - } - -+ /** -+ * @param string $entity -+ * @param array $events -+ * @param IOperation $operation -+ * @return void -+ * @throws \UnexpectedValueException -+ */ - protected function validateEvents(string $entity, array $events, IOperation $operation) { - try { - /** @var IEntity $instance */ -@@ -769,6 +777,7 @@ class Manager implements IManager { - $this->container->query(FileName::class), - $this->container->query(FileSize::class), - $this->container->query(FileSystemTags::class), -+ $this->container->query(MfaVerified::class), - $this->container->query(RequestRemoteAddress::class), - $this->container->query(RequestTime::class), - $this->container->query(RequestURL::class), ---- /dev/null -+++ b/apps/workflowengine/src/components/Checks/MfaVerifiedValue.vue -@@ -0,0 +1,5 @@ -+ diff --git a/docker/sunet/512b0a7c52640c9da8905e52fc906e72.patch b/docker/sunet/512b0a7c52640c9da8905e52fc906e72.patch deleted file mode 100644 index 60b50bda..00000000 --- a/docker/sunet/512b0a7c52640c9da8905e52fc906e72.patch +++ /dev/null @@ -1,14 +0,0 @@ -diff --git a/apps/files_external/lib/Service/UserStoragesService.php b/apps/files_external/lib/Service/UserStoragesService.php -index 6cf34000ab0..47a6e919853 100644 ---- a/apps/files_external/lib/Service/UserStoragesService.php -+++ b/apps/files_external/lib/Service/UserStoragesService.php -@@ -127,6 +127,9 @@ class UserStoragesService extends StoragesService { - * @throws NotFoundException if the given storage does not exist in the config - */ - public function updateStorage(StorageConfig $updatedStorage) { -+ -+ $this->getStorage($updatedStorage->getId()); -+ - $updatedStorage->setApplicableUsers([$this->getUser()->getUID()]); - return parent::updateStorage($updatedStorage); - } diff --git a/docker/sunet/cron.sh b/docker/sunet/cron.sh deleted file mode 100755 index da700b22..00000000 --- a/docker/sunet/cron.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -set -eu - -exec cron -f -l 0 -L /dev/stdout diff --git a/docker/sunet/entrypoint.sh b/docker/sunet/entrypoint.sh deleted file mode 100755 index 686004e1..00000000 --- a/docker/sunet/entrypoint.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts -set -e - -mkdir -p /tls -ln --symbolic --force "/tls-host/${HOST}.crt" /tls/server.cert -ln --symbolic --force "/tls-host/${HOST}.key" /tls/server.key - -sed -i "s/ServerName localhost/ServerName ${HOST}.docker/g" /etc/apache2/conf-available/servername.conf - -# This will exec the CMD from your Dockerfile, i.e. "npm start" -exec "$@" diff --git a/docker/sunet/nextcloud-rds.tar.gz b/docker/sunet/nextcloud-rds.tar.gz deleted file mode 100644 index 5f792d3ac3f65da80cfc1310ecd169b31fa98103..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 780560 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

S7Eqc+N)YdiW+m%{Ii4oVMcvcv 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

NxIm8$FL(F_S&dqjS#R@ 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 z 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%0Aa{6o4h-CwNeMYpdT?j(G!U1gBuuJc1n=&9!E zCjt~d!T)UcwmGjpx0Ew_v~Rtj#Ko!lTI@zY3xuF;LM};-sp!P(Aw%nm<*J%c_45uI zws?2yqxFfiaD%6zgSwB`{#w@YhZ0wJrOUlSgYlgo^R+*~1h5y>2z3UyNMhE(_WS)JWg;>^#WSA8 z`8@*bO&dW>OZk3!4B)UBcxFARK7q{*ew8IBgTGKP^^Y6z?99&&?Mn&smq)*7!JnSr zxL;iu^QdSu$fsRmD3=>ATIUT>Cx}&OJWdVA`2W;hS$CpNuz$ZFH;Xp))x6lZ9vny%{Zs_N?c)w-xNsk;@_{SrfySTVdXChU$0 zEDW7Bx5+r1Q4X4i)RGXppqwDPJH4TQFg^Zctc$}=p&$F_ne|^-p5OCt)P{F0+QOEC zn?ND+wW8`U-swtOC@>pmHJxjQMk`5PPKD{-fPn3M=dB34CJmVCfO*eTqNP#tY+CL4 za?2niRuDz5$_f(*tbcDlA2<5&h?VhVWc_xLjW0XWB-c-xwk@x*ALV)%0CfzbWU&uv zLg4IS#HdDIm4Y@l6gC~k(kQJ*s|rWS*=Ab~qL7*+{7zoE0AMkJS737<>&89}k8)_2 zOzsr$rf_`Y_}2-wv~J4j`Dn)eo!S4Gzj@3hiyvMfZ+_W(I`2V~rkuBF=}%?0$@+6m zkT;|h+cSybq4k=vGFlF)a4z^u$wO>j-HV+B&G)1&P+*RU1T)|fouSROQJSWM3Md`G zAX=I66!Yh^A2=<#>=QI50Dp@P9Ff(mua}DR!Vh;#{*iB^ydR%DEd~m(>7?xt|6Zi(+?W03?fM=2M%kS7(NK;XYMiM;By1eRC z{z4rgeV=p>MzAi;&_tr9Lj6K{{ z1?3Lq@typz|M-8w%zNS7q%tqhT58Lb5{8l(E%-1UOw%cR%w9$NQ_5Typs_6wC#?im z#nZXFSipTbU~JqnL${0^ZpC>t5(JRT$b4?FdqTxADu&jO{zEm@?~vI?O!4cb_}$js z=Y46C>*uXo+IWdX^_=Z&%=HtXV?&sS!THi1cKdx2#2530On|XQ?$*2LmvS5*Mq09ECnk!-HLYt8_0EA-}D*LJknVARkWLDew9-9x~NSN)aJ-f_@gn?1O zGLZLLU#_BowjwwCNiY4L^U2rG`>`X+$M;@dIJ=@4M#j^I0RHLUauw{Oa1MJ_5QXmF zSQyTFa7l$;+O(}BWtHHXTRC1_2Bx71^ibMV^Q1KNbPU)E2BWOYKaumWIgi+6uF(lEIh4yz|I{36b;~o>VT2o$VKFo7pIkTZ5dG0PGahs; zem*s_)2N8E>&{2M3nb{e-ZUx4rS>hsf})j&`}k^_uLwbvAj(#T(6X*!FTrrkFFhqz zgesy(YEKMH-h;%>uq;4xqb%_R^LZfk+1*lDP!`B}h=F3?FJ_XG72MQ z*Gu;@(}hTJ=yV`02|SF*CL2iW*p#jfU^h~J<{IMurDSLc$y{*^+4~*OW;~E=WcQ0Ye_)|a;V1R<@YmsZe5d=T2j6Ed9eQh~2a!GOqV)HQ z082o$zYl-Lzgg_3E!sjlC>@Bc*{F{96PHRCJ<>QV%*+~4$|=uM%5GOp;!fHf8Z{^v zQIJEEAvF%Hytf#rVC^gzXD(&izULByz0P8uWMuBY7pcy+2hV$}h3zsszgw0~y=anX zQ@fUOHOZNx&`F6!-rmNLdbg%GV%Wn~da{I+r3S;%9^7pvTTPI|4$A_{w(y(ZrE=J(Ku4XpQd=-ksNGIDgVIP$y;u>!B_Z0x1fEqkLaW zdbsk0x}Xi;L1qRAGk%GwRhP4Pd7%s5bZJSL?pLsh*Ar#$hg)t<^~sHZkzm2YZWo1q zKw4M%tZl2y-y{Cq(Y;?Tquk8BQ58;}x17G7Ww<0~Y`|9BjLh@x&}M%Kz40D#*0C8`4_O^-BL6Rl@|1m} z7*89t6`^SD7CF`HjX})}2J4km&QN{`koX=3OMFza$pj|~_0YGPFPLR1t-P!skv7tW zdhr4W;Tg1-sYH_rb42d4z_*Lhzn7PLeGvfsn@X%S@%~JBZ>0e6>uam*5i2YhyfpEm z(^a~uSWa<$j!N1ksUkdb+IW5UJw6&l2VaPfV_} z!_j)E41r%jY5t)@t@mk}@8ta*OZp3Dz=5iEMSd>!r}y2q*;x;obm^?g^PQWk%{7l1 z7=tOG?s24QKhi=D$`I}v+1{}hV!_=e!m+l>3@%}fY zI6M_HyH4Al*=-8ja;k?+v$WKCaE!AHje2yfB6|F!L)+Kv;?%PxM3GTV;Kr#PW4zwaU(WZOP{&y@R!@^?dDz6S7WNAFX>uGY|E z4Y*EE1r^VW0Dn&6vsI)4Lg*_ZO^e9KdVHT#Xm?CftJ7vT0a>+VtUC zRQhY_U58bEJ+%w?*4Glb51;s24BGaDKEjM%OXE7IzpK6I52Y`!CvmBm-}S2eMJ2gg z2z^>W>^@>`cZjBfN!N!tzN+nVOFzyFiagV}W)=Rlpv5g?`k@lSEg$pwMf_X3^m;Y- z`Q=O4>mOfkfAjDIjgy0fTVB6-ckPw|d4EOf9ttOSW$qz!a^c~YS@b^T{FaVn(jc%dEVG#Jdc=TeQC^QS6qEyw6a&jlx;V%PmAc$+)6G%8#pV2X>=qgy!|N}*PpeTm5A9wvEKiZS=LO(@pR=njS~xEiIxYpUHm5gb;>fQp z=ddm6QEIvw_{^o>{?-2Tt4Oo9G{1U2b&lXDjSJC{$H8rQc#88qw(b7`;pWDf8I;e< z3Uc_A^K7)U_@ZIPW@wn9-jt#Ap5Di2kIzrSvrqg!_UW=Lv#3JOb zT4C;_<5wdMPKjt}E_VeynJkXc*$-X#?|+*A^?8i@cc2O1c)dlrZvZ14p~P@IL!cGH zEt9yYC!v%^voNW}g+4~s21ockj+u4@rZb_f*iV>_)X$P0se2U~Eh2-4Yfh=H9h!~? zLmFASkudzdOwDhMI$x{_bo+_N+xqsoLNu*=x9P((w>On|v9BIkTwPHcM)NJAieoeq z`+Cx;^Nxx^Y&o_^U6USzRF9r*nW7#8E>(@})aXnGEPXKesXXAEb&>9Y?$Ari&DJDm znhaYYZT&zJXZM<&`?}a0bWQN$qO*0G!l@p8($dV{z70Hl8`xggeiL!woa39y-Hfr2 z;f{wPoi)L`J$%25+lKrZaxe3RM{{ zX=>nmQJ90nveeWOk^etmi|);*c#_~X_q*=Qo_b+)(1VwsEiNOah36Tv+X=^L+TVqR;&F?Kj&zrG7v^^?$){#_D|sq{jgURlul7K>q$M77 zSApL8#8vmB(0njVGkzR!=r9u-tuV_8k6x#~Jv4UG*IC=RA_bav+N#&wDs~Q*;`1tT zWZP&7XHk*r=EJel|G~R_jg}u;|LL9UMQcm&q@Q2gf}efIW4kML=Y`=xX!@}mmfS7;LQ@9a2t;*~>29`)7u!1^TPSkO z*BxKtVASzNr%JqTISg{~m`^+hduZFyqT!;d~jW7GdW{O^B_di2Qk zYYATJ)wL2lGCMB)hd&RG^x8_UAh51SP@7I}83?wKeRGU2;PE&c^Ddpv2FqB4`Smp2 z65&)67lP;}1}$Y%#^ZQ|8%(KG%W6msSJ-m3 ziwQaMC+%^ZPA{0fjof1Zq~8i{^d*$^_4a;mTI36-!^pqJuUe)#E~Y?A=25hoq1x&z3oI z$ri3HqzB>qZ31TYR0(!&n3X{|7f8_Aq1`3r#!%!}h0dfaqhpl*G9#uKCQGwr7P4~+ z?UW(s41mz<0re~*6Op^sU~enX4^i!_GUEOo630D-j&20QXjJ{n!*orE)-X4P{cIO~ zVhi5|!*OLH=XVLdPB@~o`PYQL@HVpGXfl#+IA6mZTgJ>h(^*JcPrlpL|< zR@^S6@<2OSdzb>Hnygvv3ny{^+Mkc$#xPmG%kH|0!{a-H(62G*Ei>#HfAFuzr2eEe zuxH2+xm7#*%!xfCUJf&7g`Qyo!{Giz`txN9>$=vIml@nZ;JFX;1+U5rXDew!MK-y@ zQ#Q#s+^O1%JDchI2(m?zaKtICdVJA;mphdmxXCT;dvJ5_2N@so6N2wE^rn07d1}LO z@~-_q=Bp256wd|W<$%K|E&llRZ`yos$a;tO-)Qwcx%nV+cP^_^S#&l+VtzbL2@wbv z?LI;@w?kXo+hHa*YfBcOLN<0$kzq9(0)W0L}6;{7dAEx2A!sJHcRc2G*k!Ye* z1rc5WfEAw4ago?T8dr8!ey|Z9-nI?{HoOwZRxe2@!Bo_C%9p^31rlu=s@@T$(+r=x zh}ph)~$I5-5Y zu+w;U1h4~G$3A8;uDWr(Vx33@ex0Vw5w1d~5vcYkjV<&)-NR2k9V1#_wFa*{3lFTr zudMqsJK`T%gIm>OU54qhy7D=giePcDDU9Ck^Kk3#EAZsD{n~PP#ZsmxSC3+m>-eLB zUpNC@7N(V6@Z{)9QY=dy*Yo`j%i_z*@jR9?zw0vmqV@MP-@fYs-ucN`cPnWJ8OMNkS)t66I+ZKi7|;`a`8%u}RN17eM&8(KR67|4Nw3(@IP z@5Oyv6yRB0ucN;2*CL*Hi;O(2q?Qi+hjMr-L&tYJH@<}^hn3#HUdcUl=IdD9rFX^a z)7ve5Xf$Be-dI@GDgsm@)(=Z}Hq*}%EiC8aN$b&8<}k=p8iehDMXcniyg%X)f#Yz? zBKbweayiInal7Ym`RuQ0!!Q!Tz+aDUed^edmSUBq^)|bO9%c@G{^YjMKCH73fo1#s zLv(B`|5HuxjF>cK_>_ytcTCHDdLL4Mdz&Dyn41-xRI!EBmIH8JP-FI{z<07?JB_nd z^o4d%{ce`;R)WCFN^QocnXK~%G6%?@DRznOPy%dVfmrVBhOjk}@GdaXjS>6L+PpQ$Lf z`p2S`;KDqE8;OWu;YeR9^Uyn9aGVlavrJ+v)0r@-{gE{eD2o>mjj`_J1s!zr*fl z^!EBF59#IO8$*`a} zocC!c+3b7;YvwxJED|)g5K5%C;J1@>o{tkxvvx*~7b9yJB~U;9zBtc*+Tdrl_S%u( zgK%FrARk0OtjNcICU2z1+7d&QH>{q7$Q<<=wdv`U>7g#%7P^-~7sIvLJtui^JPZ0- z5r9-In;Axs7drJV%EiNKeO^XVB}&N341fv`H49ZoS>E5d_1ySEUrz6SMDlIb{^$GT zjm-FKE=*ofcQK$IgUY8csAjm&h9)byOu8tg1{5~^tjp?3G{5)8VPs4ZL3iH*W% zmW^}@19uUyzQX~VOX2vuo%E()fOQ&AH$u3^$%oQETr@NZa~nKWlcI5(=My;i4*y02 z^#`ddMn8%-j7BfDdpoGhOCktp%vWHCgo@B}H7lc6@m|8*il|r3wm7i{5II8a@N9aH zH1k-mO9?K|DYt6fAUav5#A`4M`l1mZr@_nKhHF)qVX5uyc#OA!WsHChIK<%*^s4_< zww2z?&HX(m_sf>%Pg+;df4IHl|Z7? zD2M6oJcq8+1qq82E;xg$k^DCTfq7BsmhLHNW6X=3Nbf_mor~(U>=8|aWY;R%0}4Dh9Ph2} ztoV7rahP^}`Nomo2))5GlqR=oM-Pq+1*qrP*KL0v`CUdZ_LAw`BvV#3vRVss>u4RR z#9}&Qol#p8V3f-=7a;;Wx{QzS;xGVtnUyuzAS)xJM;6$W&rjFA1xzNXd#?Wje>u+dL23xJm+|D6O2=}>!er|REl@DDbH&tp z)5e$}?+N0vuNmRO7YD;OeW9u-xM|1|=vE-t^0T8~>j+%nEL*lz6arLbPKAbVXG;y) z&L79(@@zO={8WL%2Hn3|Ug+!jE5FdW;&a;%{8r^yWUwg+;pCjCb=BKL_^hUmt8W)|<%q+Xa{q8z#5@K>j(u}jkmxm^mEp$Ddp=Y;@>te1jMn$lR5MfK8ee4P6 zWmad$#=ExJs(vo`{FlNt_o!WA`Y68^t~mkzc6nYlyJXw7JtFv$VASOXRvGwAECTpo zs%I;>P6wbB%$h()+OEp)qV_WDw+m5|A{78keP24~N_F=}V*6;5JziM&)cB)Sxij>C zg3}wRD@bpJ*AODj$RYu@+ava-Uc z;IN9F_TkE0@W`27;JwRO6zELX>T$jxmBwfFqXVs^Ri}3~8at?P;6%N_EW~i9P{NSv zZs8OG7mG1G-;4suZQW~olCb{U#y;pfA>{`IuW)^Sy$ROnpz>+%I;o@(P#oGFqe$ei2ZkmMvK!=D&TmRw2% zN2#9Y)^_ke0qaI-a6N|SYw*PJNi*`oaF2Nvo^%`%O>1#**Y*<2QmDh|cz?lkcE;?E ztMal_ONvJ42&{ukn38i(+US+L-k~kH6@6-TriHb$&0kNbe_-6-JmUNL1fM<$(_7^q zAB^-wJ^>hKfAOKA+y3Z(z^Z}Lz^dVS^GHUUNZL69k!Hk3RnHwjnv0&YJ_pd!;Uhnj zT&xz1Sv`e3C_d;yyGdn{reX$0q5ZD6FQSw2N_C8Ps?(epd5KDWshG?tN&boNe_xj0 zX$=vIx6*6!967S<*`iZ6n{dTx@-Zc*P>r^$Exx5d%y(e#(3a<**&KXgy(^P+E^lQ6 zu1!aqX+Tspv0liyWt=EmGGe4-GkQJ>q};0iuI>Lqga$&x2FUB zSn}vd2vfEqa8Xq+x|%jutk{FybE{o}m{7lbdO@+*ypgrm1af`!d=`ryder$Fb`2bg&an zzq+suDR!i5+iEGYj#^~kX0{723Ck|Sje_yz3R1Q1e1K37@GhvnALYz8wMYCAU^V@8G0s{n;WN@t+u%a1=VjiXvPxOXZ4&UVAsCKnQV_O^0cAZuLvqNP zEVgOkPGOuzKCkGtRZJ6vR$pIT{A!lW4-|&OADZ}i4kU6XkKtvw-w6$a;9F^Ot9Epp zk9G0RM0vr`yeN~7go0&txa751!7XG>h8@mg91Ji5Fkf8Fm!_(f0rgf)_Vps^Ep90KQ){4YolgCc*k5=blnrGMc{B@r2ZG3)Ex@~fQE8eCF#LOm zoZ{3?j9I)P{bFvqp;^oL8WPtY7+d&4M-u`m6+=q3wBn9B9AUGZIW#z_t~T^s%$``X z%&|bP4Bga;FB7*cv$C3WS6ADzqo0>zJhS}7*R!lf5Fb?V=?^Bi7L49w2xpmIh=kFs z3dK+W{z9&fC0X{~p2VdYtGGQ$mn}V)T*5nZJ4F=JaQA<_T}hXs$`bxBZ}~hn2nf1l z78yik(#1W8d5}RtWLUi4{uHgDyBw?f+}a3b8bL-Tkr@#gUpPBWaVT>Jx?Cohc*QF{ z2d#QT)>Sr`X1(mhw1cz5_Z-i=dZ?o7>p3{`Is5SRl3mS8y(B7Dq0?>gPFbYT^+@3v z!^=#Ib3_rn5_Wm{e8yXc9-qdz)!~cOo|-oux08e7^m$V=zf-+74-g6_P~AfY{A{&g zoT5#rok+5~p{4<)h!YS7PdOo(_23APgOP#ksJ*lI$p!4ydrP3jc3DHv+zC?3o*qi5 z9C%vwbsl``*)yjuPJ?nB^X4s+`BojI@`*xXkr6!+1 z)B2GudV^ti+UC!HzGj~)m?lZ_N)rc5f8NsyadjNPjm;){Fn0vdq8c_CD)Ag<2_u~r zHciH=I^&&ESCRy3$@Cxz9#bE`_+`0*~8n)_1Lc>D^mw3%=@LCPF?u+n)8{< z>FkJE_d9ye3Pr%$G2!PDZLb=2wP$)0llH>p(6u(fI5Kut(YDx@1cL=3o912vjc7^1 zJ$I6CcZ{3NJ--SW2As>BnWtPBa8!!TZrfbv6miqG-R=JVN<8=le?KVr8=KR<`Fz#y zh(7I^n>i^5z&m9-!HGy5+Vx;!g9Ts6*^1ouC>da<3wBS%vV(b{HqH!|psbDe`rCw*Nl0XFmVN%ADq7{kUtujlciGe&KcU zuz%e3&(xpxvkR*%bdIkTz>f`&#*rZWb-XE9KT>U;s z&2N0VG3l=QXR1$U^I*sQ*Fp@J$UrI~AoQ(iT?>?+FV~=6iz9XJlkRNH2bsoT_Do|o zIbVcSKA?*!7n`Gl24^%Kv35HcME0-=lb~I{ZajWp&>v%XPteFO_=n2o!lw(JbXWZ| z)u%J<9e33n5MIw{e@k#nrPdF-RiQNqf>5)a8-xt(iE!!;Ks=eE&|-OfG1Rmcr^aNi zFXnRtau;JH88!oM-B1+~X=CQ!)HZM34vqrK@}_?@_xqk1$Cmj3=io1B`g^=TjmWjH zUt1G`(R^x->ZLW%Da70I1ZCq2C9#GdFcAvC>_TTzCrB1{2`-F*YMDiuL+YU(nl4?% z9w=5;Op}l$wyii%_jOtU20Bp4+Z#Dh;_OOFI&vImQ?< z6YcdBNe4ro%m)$;#gfDXTQH`T;LhgmJOI&sk-$I6LV9O$3NJ=Qm#Xr#sk@k$@vx%3 z6-GpQ0v4;)2(=Sc0=BlHvyE2k#u%m;W_qym+N_5yABV0`;jn^X^^~n7d#!^j9VJ&R z25tw*sFs=J(YDSl;ztgs(NQOU`%K7ZE+?UM7yXXp(;1ZBPO+l8VN`P^%`nZ@hkg$^ z^4MuO*$ylMTPTxNDMUSa5mn+uMs% zksJ5n%X-End51iC<(<7qs4Fn^V4`B*cT<1m^)WczZNDS`>=2E4q|LY_T-)#@=C&xV zsT7Tn`s!$No4q(#uEmwKwIUrvdMgIx^$6Kcjg%H6CuNjy9$E~O(6L>Z#nhlIhXZIi zcpVe(k12h3h&~VGE2DGy)jiV?{5AZk2I<&zLvp(4pJ_gw>RHOZih8iC0EW-=@{V@^ zUs!7cBRO~j6Iu-A88Kt$CR!dwP`!fFTWXgp6>lesESSf<#YCgA7=Q9r0SzyXXzB^L~tw9~!Hjc|}lzXF@%sB_e z;!N!uD3O0|27fZ#xtaRzZ_oU zFggnW)M_X>6$Y&<8B9(YjL@v3O+F9{f<;k`6R|O5o8CZxRCzWNJ#;8BZKO_sc;9$c zgtJ~T$O~iOdV3y>NPy>VhyUJsv)`UTM(T$N!r1pw?f+!eiR=nJrOzwZ`|Q(~FFtnu&1SqgU%-B&0sl1JHC-nmIVdYEEc( z-hS##0p0O7|E62va`@-|JRMyL!@MZ&H$ETdjq-j+_W2qD#E$BXbWW2RnCG-j+?%H^ zYO#V2xp^gl?9^C|7a=gFMXM>dt_#-{JORbzFx*Ttp^>(p17wr9;_Demsni5(^zW`v z`ykIsKX_HxQE@tsS_GkY-?})(T7PJPeqz-nSiRqMf!wFf(G3y?BeR*KLl;610a?4E z!td83a748vxM1c;nGy$?ZpN0)1WUw}6F4(`U0+Md8Yz1dNN*=|ij?vNISeJEXL{l5 zZnG1l;XC{+*wrz;9SatKq>3 z6t+{mQJ}hVD@3W6!8WN~)Eh5`!Ni}=o54KJgis}+CZCG?CN~LPnhMw$g!8RSuErZz zQp4?Ru>e}`*vhb1eEKy0{`+7&CAqKj{Z+}G`E{@ur;GlX=&LeYO_)Q9x2p8LH~tNj;?y4fGwlBM_suZvHCMqs zu;&hg?1zf-omZzQ-|zZo>d$J*YE~ZD!BRrh0?FOBw{POAsz`0HEka3~ZKkdWgYfABa; z=Je6k0eO1u5O)eoXG1F*?gvBcXnf-U5mHSmZBlTxNi`=xMx-cjl;xIDwB%rBLv@}c zSV=QzP0$&(<3v1O0n=sdu1p%$X3o}pP#N{ZLF8BRH@ab#>U9!nZ7E?QL&9h+})eyCg7J$4fRuyiq)M?_0 z`fjhOkVuH^P|SKYv(5vSbA~xE8%}1WC^vK37ExX$w!@fMuZc%?SeoUD8Her361F$y z(IR%t{e4{wU8#WjvXriI?c{dti+;!QSvj&1W^-8E8;?Y21Sz{@3-LpmE3&Lq>DD5q z!F=9QG#o9t#RkpCe$j%Nnr(KRx4~$@y^amm%!28O3A0PMzv-zHhahz?q!fn8iiZv6E1}?c05vwrC7kaELm%g zj*y-<_0YmxGTQ^ygV)8Q`SOSN>At@HVW8tPuhX{q(`~1>pAK};bWjKdg{lQ54jLxF z4-H60IjU93_NbW{g0#jJh*etYGgviFJu|9>QcF@_;kNz~Ee>Rwpqg3r#C4ESB%jW_ z$C7f7oqAwIUx9#rRYoT+olSB4Jey%pzf6_R&&XI&!mzd1fsx89qzX40UM0?IvK{SE zDj|FHRBdswXlx6c5HQ3C;Ag!r)k3kzF4@JbIbVF5U2G*WN#? zH~96nddG{h`NXQj*L%O~cf_9+lLJf&IMu0I5}+El+hnG#rDzPT*5ndhu(e0X5#N?2 zV1zm88_t%etqHl+F!=Pm_BCQB7#~2Fo6kkjr#-{2uv}ecyM<$%8(+HM#6q+p;HcCCR(Q|;uIMit_rHGLqsZ*dgX0Zd+4vvs@Xl;H z)U(q?|4j2~C?k-kX%`$y2Q@YKJu8!yHR$-Jutt{NGAj+$KMd1M4PB3g%*f}XgjTZg zdfJq`BE}pqhRjJUOx17`q6D&D#ZDOhn{MO}hv@wv@K3{97d|J5wDVQ}O!eti{g9H= zIBM!Z7GZz0@b#olsC9$Smmx8q>jDSjpvDLwkLiPKq2}^*xszs~bWl=ZL)h8giMMdf zA4~ZuTHbLDmt+I=i|q#S*AyM$wz}y@L*1wOfU-G zDMrP3bm0#+9&Cdpe<&_@0#Nx1ozJ3{L04;;`S5dipRWFz0251jp8DqeD;vSedaA%&;AREx3mC z$$Eyhv@1E@(kXY1&PnTlx6weGtFX2KB}2!EDehpcp4e(mj)bvN;@>_)5}gw|;9KcC zUqed&`RA0@bRFyOB9{KKtGVlZs>6Lv*uVdz44-b;Hy!6_23^vvUjEq$j&T3rv8=3i z)AuG<6QS%ZNzCKk=yP_ELY^|H(3e?4T?HS&tSTkrGE@%+#DUI9Ss0>gSO!=`H#CS5_pbFsj# zEqIGwn4V|D%^KZ*%*R`;3M)*_yz#>LO}00td1M~UOWw|(wY6?k6hH0<8Z(LC$zFYc z_=LUkJ_^^HWR6(`zq&%^pX!)@-BO-g=jFMKdDjfRS;G9oKX^j*fUZN3Z8!dVo%5U1 z{38H&94_bv$md0EzOlPOstAtbA37JgLSJE4&#mI4Jl?1FbwXb=1#i_jIaNAz`$x_F z?q+%~<$06*p>!|LhO%!V5fD_wi(SPT?lhL1F2Qa9O=gf@urQd`YSW{L zBo()MYOXtKcZ;zD&1m(>K0FI3bKYNRZF7)MA1AS24B(HL>`@u@&(FgaKZ(Y^vP&%X zR~|!x66G3#I12=#w_YcxyDS_T4kazXdRw#2%@nm@%qX_?h?MYA%803GZ;}x)1e9SU zipB^}+!-;CQ=#3nk>%E+z5W;c>D}9#{zcL^QaLJM{?Xz*o}h4thdoEjbAh6Oi%$?aPm2|o#%*0V=%B-kqbeK z#FVm1@|%a{)>JKWyu%sFFR2or=pS;Jc5mjr_w7HhAMVf_rw`m-p8UF5eIrS_p5S{O zz?-YM9v<$Y!XJCe2V{<4^F0!e`T#XXH3h2G^omvu+q)=u^N5GN=zcWb6XX5a#xN?`){|`Fyrw4fEw19_xg)i+3)XpRQ-*L6)lW|6Z>o&`wvB4inkpV&|8qd z@743V16y;UEDfXBFrD37%Q_9y1&5+)92WJbn|N>bQ_~H&nYY!{&Zs~0nn+pnATq0 zn?2mB^iXJjo_y`D9@}@r8TmnP&#qbgOOpAY`*DYy<_0z2Y@w69?7g`cBk@liX|t!K zWPxVC7`n4R>D+g8-K+H(zcKq8>4ryd>hhz3gr5pk*z+ z{Kl@sd?%r=ow?c-$!fjS9*>fo-mwvxy%%mer^v+t!NxH#3X@bjc1-)c-p>kMeqA?g zJBmZ=uu7qE9PL~$(L6{c$?=Q{^w!sX%!R*UN6jL3Y$*D9t3PuG&&zd@&k7zkNjWbL z-3pdnhOc@zj_G`RCpD{iNDXhGcx82Z^cO;!tsM0LHyprvQB*hb*R>;Wk48Ri^~-y_ zs`pjV+=Q6`uFCov;E@4>-S8fLq5bQ@^}l~gHYsX;bHHhy*95=@V@i1_fORg%~Rr62PUbz?>ns+zHy-Nq1?TDS~XU`_YfOzyd3I$ z)T+(CY5I!WQF%Nlw_2Ox&;b|#2^B|Jp>1asOo*5p3MA0>8E%R1u!^36G9mz2A`eny z&TprKB1j-fo`r)I)?ImE>&_#z??&NB@4i<1VbE#n5+Bt2zQR{U@BjDm`q)QRD*_?| z7_LrKZ~-fT5v>Af;7=zDXaY_mtz0n^6PGn?F`I42f;(&j2D2D-@-aA6+ZdN~y-|ki z)~Wy_QWaoD-CFkW+a1Q+JMB|5Y7hCyw~pfpaJ%O{2z!pFg@Nfp?uA6<2QGD`R4;saX`p<0S^2W<+rH`5nR}T8~wbSBPR_Rq3h2uS!Dnr>e zN1Ch`q+crIN(!o7HWJccTC`mOc037NRbJ%@c-n_~X5NllQ7l`oVIp>5k;bsjPETJ3he%iq=VH+jR4(()ww>1vjbqWkz&T)Sw-=ic9y{DFnO z?3Xdv=e})|qjEzwK2g%U^>tW+TfUh4Nu3X9-Ph<>c`H_Q=K`&}RCbi!AARM&i{53J zN72Oa3@v?QHoY6J@RjsCjjG*>n%<<3z4g0z4SKYnRC-2tXIbOnK4=vN5328s4Ax$Q zlD;}@&pzXIuK3wkNNcw~sC}I*&@SKP!RKUwqaD*@o)V9}XGIIUX75Su04qQ;+mt?5 zTbOxrH~;Z1#3mwpdgnKcb+kPpQ=B8Cigg9 z-&gfTtrv@B_Ghu(+VkO4RS!L;wJNw0WOw)s-DQJL=LF;8o52(-sUP>cH!{yp0jt$z)gr3P0+8IDPapHGW`IUuzM)F}v}I7SAg^ z9BlFa^RQ1r(`txMbtwAMR2=(#bSebFg7NfS$D7fmEwXtw^f1bd*RHbN5^agX?Rh9C zcs|^=N<6{aE?v^>FsZl*6ztB34B3q8$x-5i-}a?Wz09cfaAz#{c)iOCJ$Ju z3vBy1%1Z?&YyQTk+bLK(+y;XH3PQPhYuBDy7zeaF@vsN+-(E=GJl*#x1%8XYDbf4k z&7b>Ed%e9SZe6c3xhVS^7WKhZx-TU2UFW;G`U~qizj27yJnVaGFjZ;L2+y%zA4#$j z%JigKVe1ki=H+x?k^EwqN*kS=PpGa)U^OL& z(x8^N+f}+6CaZ#w=3t-VMmG4PKEtQct-te8yf>i%jQ5+*eH1`9;21>MNUXDga}9;EpJ+He{7{awazR`(V9hpW%=glaNfctO#sgID@zu|$tJhygTaEi7G@c$KTA znDZktm}zqwa@fR+d4=3Uel~ZRwLkGjE@YJ?E-(613hcC~EEb6J&cXO^`W_8)IKNA< zJ~rSQ+taP*Xx4I+ucq^8K!nhinzN#U;T;7Ww)EYITo2?zfXAWhw%TCWhfOa7+I+yT za2wbmyB1@L?En-RVoX5yc+gNvcosjmIDYZaU$RkA**ziFzP$9_5 z2XM4g*c3zwF9A1_Jf!9uYGQWWaBlsT;Mo7L*__WK-I;RovzBvez_sz5ZoOumJZe7y zKh6+tvmVcr<-+H6ADC;36WVmK<6AjuD;R6oGoUoEq{^W%c4A=xibFkJ_!Fwt?F3cO zQSp*ntvd@MMz|o-fMsO~RJcp6QLlb%mg@CS9vz6qN%695&c{1^Bon}eZvqVF%0<=Gd z^PMs+m9bmkRm4YGF4Ezs9s4VA+M;7&O;iBYvAlT3-a8%pH+$FStdid)bPa82#QAjh zIa&uVqJi2OK}KlK2Q!9TW;94F*>%-td54~8co&~6U8Mzf&H1tvBRd9{3k#t+Oz9!KE z$@oSQqRIfIeHL)qV=zbM0l6rK0iS`Z3W}M~9&!NejWz|F%yxZne~8gUDC(zF1TZEt zvGb3@`jnsbw>s|E1~lS(JQ(|_jyjYbdd!*79GiDp$Z096L2Jm;^4mLpFmh~2T~|HU z@3e7YIYuV%E*^SJkr!L8)bo-71&TG%?b?O&5LA@ZDcV*D{^?lXk0RTUJNLIR{@9d8 zv_BSl4)-BPxYZaE@m1d2xs=zD6iiz)Bq$B~HVKAZakAV^X1LPoMDnCYp?2V{+$C|! zbXzhE8Py7F1@wFr57)+WWYP2SYV(i!qTh2J{}$ZG7Br%Ky!RZqV|WTqoeIa-fX?aS zrUW!aDaXugjuPkuTWuK4K=H+FT5$0o-wdc6fD~rXW}>7kbW4vjL$9}_hf`(RWipm>w3F0$t?yidEl0}B*@$b^`p+1aL^(j&4Pb`Yj%|>{<{Iw%j(aNy}a>U z$P`;}2)10`834 z9fT@@m-8@R+9?eJf|>*0pSJs-jDKDw{r_%{e_8#R8C>3Yvd`zN`673!j$%;oH0^IF zQ;mB_>U&*~&kTLiCNv98s!E4e!c>)a6x2h2F*)kMNYx^@ecFWAQCn|ydT7`3+&q`2 z4Lc^Cz5B1u-u@i=7t1#OiUGX0;MxY>?>(7CW2(C9TTa}=^GL!3Ce=B1*PVs(2xt@O zHezQgiHk$e+4WUi&gR;%wvQ(8tKm;}_#yidc5`hi?XjT4La`~V8w2zBw zeizSurDqWCe?IAvA-zdLbt}KOj;V6TD=xkpOQ~C~jH0(KM5L4VU}IPN-aQD=v4N1z zLj;|4B!k?o`_Kp(uO^eJO03hhQqD&@?UwC^Lw3*J^ABg;ye)A8{*C+$z70#DewU+t zY`_`rA8$R!&3a>p8+rs2S11Ee#m?jdo0HbEG~6vIi9x~4CvQJ^lKaG55cXeOfY!#V>a+dr>;I2*w?^*W`K_ZdtUh&sOOho4)r|> zXqRON37loI=`K}cHG)KROwO^ii}_HG9t4hwRuZH4LS9_=Id@x@ z*}JkPMVYMqS0?6`s84qEZbU^;c2L1PKsMPo5o6;0?Vnp&R+m&ub$7inG1GlffS3oL zv*gK4@;P;nAj@)x8G8j|d^%x`B~QPvTJ;&W?_PZSp0M~P@>K2McJj55s2td6M@wqX ztQ^G+w}CXOVE`Bn0Nx%zn;x#D1DNSsSVgN=(-U2rHlmtQ8IxQM>X=gX9Tu;dMg}q~vw0rGv z!}4-JJ{XtUY<_y-Zy2;Or&-=BMI+kAzG&}l>q3gx$~U)7E=ey$w6Su%W2C$Gv28ST z&(VCJf8QHb)`_qBA<1Qu+HYKy4}V~C<@{^gfA`NbTUr{t?R9_n+=U&zGKdk!mL769 zJ|yq%;vm(Zuj_D>adn2OV|53}Zdk~JRD@mHTRjcO*+drl>KsIht&B*jpwb2i4awXd zZ8Az-Go`;+RvQGZCRUZK@u`mw_L~r3l|4@H60{tYPTsw`Lq`|*ml5Pm74>RAHp=oR ze0cwIb0hV#@TE+WW6-$ObUm3@3$5lCNMVgM3B%;dN%wMB#Oe58gEr#OB!1<0Y-QJc zs?@^0IP_F>_+ew$yI#Db86|mh8uz|9Mi`o}w0^Dr?F4=pUftx+Q*mDPb-4>`)6uI| zi^{wZ72hZ+TKR`>hG4u>F+nXvrMEkq^j16Z#KQKNpX`s7W4Mxa-WfnEt!KS4KM(j_ zVsxC8zT>|!_qN=7squI!?{UX()#``O^LXZd5p8d}Y>V(mOTJ5RkW-!HUn%2oZ$_8N zk#qTv`jr5(t@$$n_Khj~uTv=utYiQ1p+L+Z?=9X-07O8$zkKUE-d*n2lN2t17=s-> zJ*1SJ?+9+WmZd>5K9m$c;C6eE3vGWhZ$xIe;uN6>6iMwL)YxtM=A_ZKTFvk9XcWWS z-9qk_t{HBcq`d^=Cv|eZ4ZVxR59nPkcSCQ>>k@z*2ztCm7#Ue3<1j%QK{V-O)Nb4$ zhM5^rY==0UOp`F?S2jfiyOgbCAL@sF9%Eup!SQHNBajh8RDq?Vb$g}8R|fMlZ@HD^ zKdV;_{~!A0s%al&odCN(f4Q{(EL8B=LXk;4R%Mr{!eLDC%BO z$J3Wf>(43*!xS$r@Rhg^_2Pi8G$I?UXCzAy#e4#-hdQq-C0pQA(_;iK=gTd(J6MT{ zyA@(hLnj@{sHqaX<~B2=3>z4r_xPHURf9#;wnUm|F0BxzF^vf>SQZM2&_xqUq zbXMvksaFL*`sf8|;hK58CQK9#4M#e^0ATSj&7(Q&8JXjZ)8$a9d~?vN;p3og?^8Yk ztufSF0U6>S=8~pKYlZiY!^w2WAb6!zx}Dmjjn*mMS=Yz(^N7j0cGhdQ*1{kDj^@90 z1oA#tZk=YiY=r&6ETFcr&%KeIIN6a;`RUsx@g7s;=XP1%^7WIM!~gjAzyHx*$Kd@l z{{^;SnyH{UR=>0z8ur zM6xK1QqTtp-Ul$kKKP3zU-u%Jj-F;IyEoHkmNKA+vngGGyGcAEcRPMLx7-!FVzmy< zi3ke?%?Db}h1gUNdfRzGfE*dS_+o+TOfQ>`b<=fl-4xNuF3K3i)JDn7m*)ilnMz90 zOIOl6GP+vRQ| zPYY7FlNazSTjDG(9A^eQ$sEO4_16TC&1xWQm{lJiOb#l6^e8O!N_}9l(`30ZY#8k>U&D8Xu=Y8e2|sbUi=HnJTr{jQXKPeFGMzim8o9hdd)R`M>*c7?6+6QkMk zCLQAks7E$+W5POGAkA((w6&?w?6Wbz#!JDEGo0H*8vsyNaJ}G{2V>VX^nQZ_{q&+!bPY=ON*VX5XR z^j;y6YHSvydeD0o{MZMc%{*^|k3Tugy9hb0#$IO3yAAC?k(F?HQLZ^~Cm^LQJBO){ zh9$cYFhc+W)~!8s5g{tA$RLuZ(u!3pc@$IvHbfytJ=B|(J>Q6QsSM>x43Y#(N1vDA z{7{6g9i1Plvz=1Qk7?DjXuUOb1ncf0uB|?c*m8lFmC(26#x|#H^1u}ZwGao&&Il&9 zt@seR->5W}AOO@XnmpX3eP$vD%0>%01}@QHPr*f|FUEzW1FIRiKakP-i}g1@uY}i< z4L_NPU%+{5uQj_qLFpx@LeuA()35LJwN`CwEq?UBZ1y{{*=K`STdBi#Z+%n(u~R5} zDDsHy!|N|lm(em)^;OeF9KenkhKm@uu@t5+S(9qGa7ERnYr7aPAbZZS6I37dtX@Hx zQ;Ps4m&*{y-^xz81Z843iUwAKAKiLn3+=PK$0s(5!j8xmMnHNHno;a_uS&eCCDx`13LcX-9Nbr+(Vi~hd{YEe!vcG1Ni1_a zsz`QeMk8C?0t?v)HFCo5djmEi!r1e@a-?*h<9q*Ou2os%Dl+X$I`j~#Prh0kMf74e zrTxl#e{J9yFL>XebBtEyX=h%V9PxYc?0wtYY=26%txs@Ncm<=E3lWD5J7gVLVmDN?CFav&E}KhhfO+fKizjjtqrs{-MwE4$;;Q9#!_ll9=NrxQ zozyo3FXtWMb8A1KaLgFbOnmVV#ip|hU*Ve=Uef+ihTp{dZ8nNyK!LdeX@UWVsXRI z2ePrX>;@ZwH6YC@cjAUTm^eTX3H^`=$0k7x<8ZD^6k!AOsh?j0Nz%tBi9b z7_egZrGV#SXy?#=A0|Hg^1iIB-ia~uG8G4ZC=%JhCVuQrzST0`34gPXcb7X_iMUG> zVd~Fn%~o^8>19r}nR#`%8%ul2wL_E3$_#^7D_(!2cLZCR{6L^s88nh)SWU>@BWVV}% zl4#h#Fo`Nf6@6-8laf91Xa@JQEyvXIU=|58txn^(mn^5_fM75tEP{%w7rnIm=f5$WVCc6Q(^cNlfUZX!jmDex35gNQAvZUv3?EI^ zV6D&m-K1WRO=;ok8zh@6#zy4@Z>bY0;5s6%0yOOew|5+ zsm0JLH0>1*0lVySJQ%OB;YL?8kvHkJhtDAnTSoI8rPWhqwH}TSKt`_iw%mQ-^ILkU zs&u{&_LHz9=%+s&As+;pwI5D4oafh4K&JukC;9r2rPDk)W^*tz&1rf+Y5?I7yT>f0 z;f7$SW%R@Co5a&p#_Ms{gcy*E zDqV~qL!}+LKN`Dnv6PoL zE`*d#3gT+4kD_JJ{ne}g*nZ`4uJKF#(vx3G(rzG~zx0KyxIWGKr1{Bs@?Yvb?|$kx z`iFZiIPiMd(K{$Y%H~>K^C1@%Rk+0gfymJ)Du>}{p;+Q`R3t zkzDo;6pWDpHlUd8q?!(yCdOS|Mm1`&Siw1Uai*Gcr zqrg`KJ05m4uw@npd={_61vVjZG)zRBt}wg+alj3{C~OnU5#F z2KXaE&v#r!uEuo%@_{Ps^@-WXG>N8vb$0ao!td9k--kfC`+Y zt8EQzW)%XvI!W}{D#aJxCJ^@nWuU}~xjx8CmRMROJs``0IU?CfrS)`85K6KT)+jIK zauduw$B|d`&UdAq@HYjfXXjh~d!yQY|NCurdJ}pv)Z1xCOJyW8S*$qJo*z~#fbVVn zEnLV(Ip@5=K$_TLf=o;}12Zkyi87-Mw|!|}Mm6S_bv6f7H6W`6GeNQ8UKiCQ6H69* zz{>u+E%l4TrXPm7pDF9(j<%}Qdpz4w zd9)p}8!9IerxE+{&zX7Q(Te#)Jot*vNF5T%-nhSC;6<)6q-EIa+l)~3@s^jEhw zUE4pc*j|p#tJ+WRT@v+S>8S=B!OQu++9_jXERK5923P=vFof7)rS_vBlB-<}@WYW} z*xEz_c-mT0L@w|lo>|6zi7YBRs*O4T2tb*!ozs%YkL>e)&GQQFU#GXXkcY4F$F`f4 zzH!~uetJ<2dB$4}E6G0}jr5O@ebr0Pt)CTe{QGil=~?(CVc+K6!zkgK_JyjN`qF*{P;hjHynSM{KR%Yv9I&i zub&v%*Smr@5jXfwC+FhQPa_rpenU?x(N$_(F{^Ppl{f&zX=!1A=y;7J0ELf~gU0Q4 z3^wO>u`$Us8_<_FhGFs`j`68jq9E!d1zKBfT0^ihZtV(;&E~qZ{qx@OtC`HT{SzLS zqjSIb{Io5An?5#1(EsP{y1GQ<}ouo@AL^nD#o{g3HJi;o@UDLbk5BA{mvWu0BGkf zlSUk|vmrRIRdAD~mb)_gnLSeuaK5cmA7Y3_VQv;Mp3EV5?k{H&Nph>4+S)*36UZ^& z^xLSQ#_^OUyNFrf%|a+%PuR2~$4K7F>KE9-gWQ;Nih5H0!eYNA=s(j(gw2;i!T*7g zPZ&N=OC@a1k_el(OEXWW7Nf4hd(AGz5W-0MeZLUd-F{)RK$9fWzG0vki{pa@@0LK) z(OGtw@B+s}hCZ^*NSUlscI*2wa5yp}UG5L#RPaVGkEz}Bq~Hu^@pgL5xBwrZk6gXk3MUEe&DmPx~jegO(kJ_#Hz`VA4MbjOXnmCUA3AulB~qOY22i0IFcG6RBsG zFxq&G+AFI%BetpIq`XiTql1h2xGkzM%)yu-zifIJv$(+d{B?NkJsZ2neew}v!;50L zvj5WDs}Z#=>t%&KX#^vhK;vVjH9<8+{0TJLyDr^SrJPXc$rQCcPgfIJ9CbEK#FL3G zb+X7+!MN3%#nw_&a>X^0AQ5n3-`R+NP7d!+y<&^+l^)vGBgBzEU+T>*()goWeU#h& zUh!+=y!}0wPx9dPpNr7`TQ#~z{G?a+ZV!>$FYDP#Ai*`2pjxQ;oSwiD?;mCcJ=ux! zM$$snS4LwR+$3T{c8!-;c}8m#CywHc)NMO>6Q!dnKRUSym10!XhS;Na?fo=z`)n@< zr*G53{w}{6`|%9^Bay3(-imKSyHBU9XSljsK~sCjD$)U0LW>D&nBqKRUfV`wxrR9< zdVk6~G;Uf_67#?$YaD)&gQ(xQtqmKgi}NUni?w&?@QvFG+&6I#5BQdp!J82U-q-U? z^bV-d(_DKeapu{3;caO1=~X(>ZQJn-3#_9UW}xcqNBgc1b9yF7sb0Ibq)u&^+A>UM zD*Q^zo9wt3`kh6bIo-Wz!Ri~$OeL;yRK7%ei`kb_>1Aa>bhqyUP{PML3~O?>FrUZVjT(*D@#o)yfm;^>^D z64%M)T_5W<^cPr2cWB*I{P(+W&`V&1^mWY zZDt3XW51z}psp{3NKhM;$VJhlGJ6pTGIN?^f60|0G46ZU_~Z43Uo>ufxs(RS`1HEM z(Hv97WI^xX!VCjtY9fBVoXuBzzVoUBxso@Xj&gcCMQ8Y`>P$_SCP}glVE)hn+%)y= zbu`Nm1ZfqT=X^(BNL%LRiS3XieFnfgG3N#Z06n*NcbbjAWv4-LaAzp`-%4rwKx5$b zd+D{zH9~IUr4gx{3U0lShc^Y=rQ3xHLXtk?94~X&MrWx*n2}AfsWD93&CNE+bA-*b zn5ucxs{JlTxm=^f@kr1&u!%fg=t}z2Tz7zp{d$G#=biI~z=b8l<*=spbf>%?q0ZVB zX|B!=UOkt4tFT7-f;t>YJ*Y)F2n}Y}YQ%0{L3@y0Er|v)46@jhFy5)5P8189#xz^7 zGG(?~3OTT8)R%Sd!!eu2brAFzela+CW#bq2?k7Cx2ReGeHMNb(xTN@hBa^=vF#SpC zESx{@4d_HqONqyJ!Ior@c>?Z_#CDsgoMP%Nq`M5K=@SD} z-lxLPxtB}z!a47d4EZiaTBT;~{;x&&nc^=|e*WKUA`RiHqpJ9z*d<@V^7=R~nj;0R z?T~hsDvS|o$0mTGMM=WCN~rVPM$^y^tKgMdY0a1@6oW2#p5ysyI#*E=*=^GwV0K?b zdi#d3ylq6hVpLbbK7qdoe_knhQ6?W~{SwB9n{O8)&x!$rQ);4ahkis|C4`9^SxfFIEg!qq&H%#L5vDsdc)`>j~ zwl9i@|AXAav-uPJI!>>xyX!r_fBd}7RlXB=qDB_gUXZ+*MtS=j{F(Q9k68GkLfI$Q>GMx^edX&TnC|bk zO&+0gf5ydr@Z?DwBd2k$*xaWVzKezaB-HfDmHcyE-bK#)Gxs-W>ML~fL-lKYqiVgk zbNt7dnIA?$FMHyPl6)O;^y9hUpOiki;nTe#3hDXm+887(BC>(wOy~mhnCqu8j)7HU zW@U}WP~AHezjMPy%cjz7k9DQIo@292#6rjvnX`KvZR*kx}3axWwlmyY8E!F+6|QmFp=AaC%^;IHVrv&%ytoJetb)=cgG_h@tK zK{tDO-nw=&$UmnJ`Kt#%obYft;Nkv=;kSn`GC21IRbahC6%W%H^nv0hi~8`t zw}IlbM&%T$7V@G9*M!?i2eVrsR=X964663WRp8hZ%)#3QRRsr42N6e?`)S`483&9! z0y>B`699C)z-B3F_>8t(5ZKXvxwrUQ8(x;pt$}|-Di`DWQy#VDS9jpMdlPfiSfme9(yZlOCdnsJM+CwAW14rJ;dN3 zlqzTF>yr2vCr|%_5&ScnQhc+5i2O|~WpP`_StkPM84)=fDIbqj*jcQCKspq9M5hA229*68TjQn}O_}@0OFm`Q-q5Lk zW7&VPX?GNy+XXt5Q|t?W#g&wJW~ zAs51o?MxY)7L&6sxYk;j327b35zu7D9y)F6Vu^lX;l-OBxL+^o{+)GS2n_tVT)t-2 zdxeuo#M9U8*fAvqG)UVhMs!deCsGN8q1Fm(J&H0-FxXIA?-ki-x1CgM%Fqw%tXDdw zG3TBfbN*6^R#PB^ki+A>qtiROkx^CO=Z^msORhAoT)CFs;8y~pdGz!_*apn7l!P~g z2q7GswAnO1WH>{Sh~`&j-EJLJ9OYsq2tt+Gfkkg=c#`yp3>Xv5T4iwr#3OrMHst~h zjp`vh+xa!?*(KC&w3a%igVCL-@$p`^wz%vOU)he)Kdmi4cPJmI zTv+p~JZQ}GNLdqwmdaLj;u@FjuAI(h8VGobv(J}wYNJzBR$>Yc=WU;Owb;_le(iT; zvWf%%SmhgQiBGyNlMOeFea>D6^T5o%(wNhWey=n478YM)&Ihfzm4-m)r}bve_FXFO z_HsWF@UoH}oLP8@Jz`R)r8(j#*lsQY2R2M1JswYwptZp=mn=@dBQ?jNk_R)0J6NI2 zFza@Uva^EgABT?IUNf)1?uVei*-ZODo9iF`CHrWAiD?b7*DR$7cG?K|l9qPET{>H!=h4 z?#CMs1B&k_69S6~q53sB_v4*C&U|o}V-%Nesjr{UyACxHiA zi9&dX(&W=qHmI-V zX`(yQlm`-P#YPK)90%ekO!gdVHbObYvSJ5;N$CY43-Z}&%oRB5?LuX@Dg*Z5Nq*2z zw*JoRDl*l?;t;(&Evd>VF#qqh0HgEH(Kp>a^65yd=UIDdePHW(e`;vyX@FmP3@pqk z!)kud$wre-BT8L^ix7tJ<&l~)2|QhU`xYzeX#zQ@>IG=IoW&Sc(p#GyuS97~&M?ZR za*42=yqMEu``9J@wt@C_oYkM}sxL9)LgSq!LurV!x))n|KVifzz_6@Zs#^shR5`-D z*r4?*2hMo8ZmtpbA0DX~O0}O4K6QUaFc$=3~rJ(>7_hNW@pkk(f zIkftHukoa3=##2ey+zL1z>@DVN&mvveWY^c^T+aQo({;tieL^6%uM%4gtNAR^L(Je zqe7;33IeGHn!1h@x9gd1ib6_FwSqUraAp`*bkLS+U~@*AwX|qT)4WwHRt~=yN>!=( z?HKCEKJL``z>L&UU# zw&I*rii{f)!*DfmBbSV4%+6&IbGIw~ika4hD6vSTFQ^lF8y~uF$alZm<=jOF9GKa; zjp~mF$~&)4Fz+7*e-eLGPHD2-jG`Sk#2CehBMg)Qs7N%+!_yKIlvO#zRGq1cR#vdh zxQbDbwwTPFIpRcLju=6h#h$QP)X7YWr6a0oVYl$z3D{nrW|yDxk^``t*oZvq^$h&o znFs%3eY?l_P0Y6+8Nko1g#J#O0r_a>BC{hk3~Ued@HNXc=6Z%03O$eA@US%%#TP=2 zXeQ8Ph|Tqq6pcWR%555(JgeY)i=M+m%PIAO5er>;ItQ4TzGD@CUtFq}c;JJlO#To@ znuN{X3ooP4&*8hfhJQp)pSgZk(&zv0_Z1Ioh&)JkG}%s+-QnoM%#e5IYZ)joIKg5s zTU3TF!t`ja7f#Yvvr>dmO{qFFMXkpw1Rhl&h#G|obH}iwz2hkQglT}E_W3+_=IE@! zxu&RB6LC&+5?9j~moL0PLfOvVi=B5#q*+pWul-=IDWa#5J5l-8l<_N4)6?hKnj@ z6HC&vlRQzNVd;pj-JX_KEQtsz;!IWRq|Xh*dTd}Bov_ZjI$<2A{`mOIcjFqlGu!ZA znwcA1@60x2&~tv+UvaqO3YV4^ec+|3o6n4ls1u5Yrj}A^YYN%;ludB_qLnr(T9x>a zBZQev4Hp&;TID)qO@T_I9Wwfm+ zdFLk7c3iASHt)wFBv~W!7%vFlN&$0BNQAL%wbUp#V`Qjx{ztnu4}|eN;N!D8e@Km1 zkbG|iueHW{TvuoEy+Y0xH``%d&qDh64i|~>5m?eYVky@lwoq{#6HDHvf;I*BI2cnr zkk1W;p_gK%83DF)+==OGOgpg{UKJ*Z=(VMY&20kUvlY+X%vYF3XvCId5AL6M`JF^Q zvgsxB@y2r!LK4MUOQ0%hIB{4&RAg>)0d0)qnkama3(b1P?xd6h1$_iC&IUDCl(7M+ zxGun=hQ%yhrFtbB%W4+RlrewUuCe9KDAC=*8}MAO`g6hL7w0uUhgG2`c2Ag!FVTAG zrYX2(C#9lS;~lz4#)u)JGA(b|Hqy+@V1kKHwwwq=ra>yrk;Yhar#CAi+HwiLC1_ht z5HX?92vi;U`SWLe3*NV~HA9f*iee7h0( z>CVr9A*`Lc6()L~79)R`i}@K!B)5iGXfBzN3@)&kp1UTnwMP)<&5@H$L#K(WAT+sU zY8+o{+!2IGmeMAP;}A9A)~@UPy1A^EciPRZ%u8QT_+CcL)o)zE_jt&_-y_~;5k38}FSxtJ_aMKdL?+y9stAP8=`Ldz^?$hu6@=XZPy;VT%%E&Hm7u=loAtC zm3cc=XL_-bH3O$EoH)OW;`p0txi6f0`Fy(fTpgXyS_MCxX|lOX_$Ejk&OB^VDO}LU z>B6cuyuc?e3Xd_alSw73itRI;?Hzm{WW)I2$b=c1DsFRY;8gH)V_LPKk zZYad$xX>Z(sB094Qo~AIh-=mM6{npNBNHfPWijSV|Kj|`rC&%=%d#%!m zpPPmk2K{`$Tn>7AkIwo{y72KSE4TJu2QpZXW1{vH$|h71@!O!DZs@66=)y_alu*6a z0Hdv`i1clJw*YZjl>-1?z#Dqxuh^jMEJI|iZWoOGc@E=S{MDal^gb{-i{<^(;6nCc zgz`NP9P#nenhHA>lJ&K(q{kHKR(V!db9pvF+x?mxFIr>R|j{*j8kNk=fF^f z%L)WI!?PLCv3P&TuI4jCczQE-?k$l2^`GnD6#Wxc(!G3Kyww-~A9QO;v>o&0UJNh( z_tnF_-F&eBda0+my7qQXc9A8`i;`|MV()bG;kfHPb2du^nmIdC>AFe_tKytn!K3 zYNn)dz{e`CG(5Uwmhn_fCO$K=?Sm0wf;QV0R52pF8pNm{r%RI9Pfl5Xw#K^y>e3t9 zpB4#yV$`qkf7}^d$Uj`@ITKBH9tWKlTjn^8>PO5;iKs69Q9f-k8{+wfT`1GVIrUM& zBGcki)z?+qKT5m?nd7Ao;Ip7+OMNjB#&o?X(BSE$3TLcvk<7IDbiZc5CFXp>@A4hkkqPPSwf>5&i?^(YNYZ-nd+CWxCk6mZXt7 zy0AN87DsxrY%~eLP*&kjyXkgxRMB*mB{Z K`cHJAr4)jKL106`U`5l0zmOGxNrA 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�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

X{o?vjRr_8H9 z>@EAIkCS>@9Q5@uO;KFvBKl{5YNEQdkK62&-lh`1*dV$MUp;(?l~;0`Jnz1`{8od; z5aJ7c{;YZ!s~Xv9a>CcDzf5j%^ZFCUskJFdqv&Yd{gg`*XHo_=> zmYPRY;e928H|PIOysv3ghWi3K+4!t}pz43~_ab?nXNPw|kbZ^g1ssL(Y^Sa7I(Z6uroN*z?_WUI0qg`QO?u5Hgha%yZ#8>ecUY>43JQZRyujGSoPCqw0|k8me+!Q*c6-n7 z*0;>%2533HfmJiflfW`i`L#&^Yj5eI+MJ)N0zBkZ3pcEB=NPI(7?3teGjS$m0aJgZ|O@ZK!4Hh)bjeghka$Y zjd}u$1E7-4E?UN@Y+ccPzyrH$Zr}j_tUK80bhf%&yA6hP?DPfh!x%b&qr^#_^n%pR-lK`n{PF} zhybti2Gd+;d=A$Cw>B7KL*qukxL;?3d}lf`+fS;-T+opQE77VX?EA4(7#b>(mw-mX>U>o8aX>P z@C6Kg_%FviY1Uyn((C{yafSlO``=VVz5Iivb3zS)BmjG zR4XV)1{`gv3k7>ugsn`hN(xGyL1Rq_=44?3;0dJj7M-n716U$4>sXFXQ+_ojST zTQ3<-UAB{Nbs_^)GWOwq);|+-$fk9R*(@I3@cUl4f6+3Zn*{72uAcA*g#=VyXYl0( zEZkF%+xgqG>~iq>TWR2^mYrOwb=R>(Ek9I{_z+(gR%P%t>0cm*lMYHLhuiJWU#lr1 zjQq7Ger41YC|CbN=$Jl7-z4e-R%q~7g502mUmX}{@U?MqYF{;gUz@(LBA&hb|FiZD zeT&1O<2p`xV1s$wp|xM!MK9B>={F;t9Nq*w>9hs>CZb5x+SULw5I`f;jHyHlq*?=t z5`iO*=>e{=tuzMP#KH3ESc8TC&UU9?8vxq`h?8);D@sSs=e5gq=PMLdxshbiauU{` zqGocP&q;sXfu_l5wH~L*=@&cs9MI46gS!#=7cD%MW@`q`7F;io6Nh8>v*hS#y3BRR zx$�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 V*ZNvt>+3)N`UhP8Dop_BH2{2U`8NOn diff --git a/docker/sunet/workflowengine-workflowengine.js b/docker/sunet/workflowengine-workflowengine.js deleted file mode 100644 index 4cdd7e4d..00000000 --- a/docker/sunet/workflowengine-workflowengine.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! For license information please see workflowengine-workflowengine.js.LICENSE.txt */ -!function(){var e,n={37682:function(e,n,i){"use strict";var o=i(20144),r=i(20629),a=i(4820),s=i(79954),l=i(79753),c=0===(0,s.j)("workflowengine","scope")?"global":"user",u=function(t){return(0,l.generateOcsUrl)("apps/workflowengine/api/v1/workflows/{scopeValue}",{scopeValue:c})+t+"?format=json"},p=i(10128);function d(t){return d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},d(t)}function A(t,e,n,i,o,r,a){try{var s=t[r](a),l=s.value}catch(t){return void n(t)}s.done?e(l):Promise.resolve(l).then(i,o)}function m(t){return function(){var e=this,n=arguments;return new Promise((function(i,o){var r=t.apply(e,n);function a(t){A(r,i,o,a,s,"next",t)}function s(t){A(r,i,o,a,s,"throw",t)}a(void 0)}))}}function f(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function h(t){for(var e=1;e-1||0===t.supportedEntities.length})).map((function(e){return t.plugins.checks[e.id]})).reduce((function(t,e){return t[e.class]=e,t}),{})}}}}),v=i(15168),w=i.n(v),b=i(12945),x=i.n(b),k=i(45400),y=i.n(k),_=i(10861),j=i.n(_),O=i(39429),E=i(80419),S=i(82675),B=i(98266),R=i.n(B),P=i(64024),V={name:"Event",components:{NcMultiselect:R()},props:{rule:{type:Object,required:!0}},computed:{entity:function(){return this.$store.getters.getEntityForOperation(this.operation)},operation:function(){return this.$store.getters.getOperationForRule(this.rule)},allEvents:function(){return this.$store.getters.getEventsForOperation(this.operation)},currentEvent:function(){var t=this;return this.allEvents.filter((function(e){return e.entity.id===t.rule.entity&&-1!==t.rule.events.indexOf(e.eventName)}))}},methods:{updateEvent:function(e){if(0!==e.length){var n,i=this.rule.entity,o=e.map((function(t){return t.entity.id})).filter((function(t,e,n){return n.indexOf(t)===e}));n=o.length>1?o.filter((function(t){return t!==i}))[0]:o[0],this.$set(this.rule,"entity",n),this.$set(this.rule,"events",e.filter((function(t){return t.entity.id===n})).map((function(t){return t.eventName}))),this.$emit("update",this.rule)}else(0,P.K2)(t("workflowengine","At least one event must be selected"))}}},D=i(93379),N=i.n(D),T=i(7795),F=i.n(T),Z=i(90569),z=i.n(Z),U=i(3565),I=i.n(U),G=i(19216),$=i.n(G),q=i(44589),M=i.n(q),W=i(95773),L={};L.styleTagTransform=M(),L.setAttributes=I(),L.insert=z().bind(null,"head"),L.domAPI=F(),L.insertStyleElement=$(),N()(W.Z,L),W.Z&&W.Z.locals&&W.Z.locals;var Y=i(51900),H=(0,Y.Z)(V,(function(){var t=this,e=t._self._c;return e("div",{staticClass:"event"},[t.operation.isComplex&&""!==t.operation.fixedEntity?e("div",{staticClass:"isComplex"},[e("img",{staticClass:"option__icon",attrs:{src:t.entity.icon,alt:""}}),t._v(" "),e("span",{staticClass:"option__title option__title_single"},[t._v(t._s(t.operation.triggerHint))])]):e("NcMultiselect",{attrs:{value:t.currentEvent,options:t.allEvents,"track-by":"id",multiple:!0,"auto-limit":!1,disabled:t.allEvents.length<=1},on:{input:t.updateEvent},scopedSlots:t._u([{key:"selection",fn:function(n){var i=n.values,o=n.isOpen;return[i.length&&!o?e("div",{staticClass:"eventlist"},[e("img",{staticClass:"option__icon",attrs:{src:i[0].entity.icon,alt:""}}),t._v(" "),t._l(i,(function(n,o){return e("span",{key:n.id,staticClass:"text option__title option__title_single"},[t._v(t._s(n.displayName)+" "),o+1-1&&e.$delete(e.rule.checks,i),e.$store.dispatch("updateRule",e.rule);case 3:case"end":return n.stop()}}),n)})))()},onAddFilter:function(){this.rule.checks.push({class:null,operator:null,value:""})}}},dt=i(44539),At={};At.styleTagTransform=M(),At.setAttributes=I(),At.insert=z().bind(null,"head"),At.domAPI=F(),At.insertStyleElement=$(),N()(dt.Z,At),dt.Z&&dt.Z.locals&&dt.Z.locals;var mt=(0,Y.Z)(pt,(function(){var t=this,e=t._self._c;return t.operation?e("div",{staticClass:"section rule",style:{borderLeftColor:t.operation.color||""}},[e("div",{staticClass:"trigger"},[e("p",[e("span",[t._v(t._s(t.t("workflowengine","When")))]),t._v(" "),e("Event",{attrs:{rule:t.rule},on:{update:t.updateRule}})],1),t._v(" "),t._l(t.rule.checks,(function(n,i){return e("p",{key:i},[e("span",[t._v(t._s(t.t("workflowengine","and")))]),t._v(" "),e("Check",{attrs:{check:n,rule:t.rule},on:{update:t.updateRule,validate:t.validate,remove:function(e){return t.removeCheck(n)}}})],1)})),t._v(" "),e("p",[e("span"),t._v(" "),t.lastCheckComplete?e("input",{staticClass:"check--add",attrs:{type:"button",value:t.t("workflowengine","Add a new filter")},on:{click:t.onAddFilter}}):t._e()])],2),t._v(" "),e("div",{staticClass:"flow-icon icon-confirm"}),t._v(" "),e("div",{staticClass:"action"},[e("Operation",{attrs:{operation:t.operation,colored:!1}},[t.operation.options?e(t.operation.options,{tag:"component",on:{input:t.updateOperation},model:{value:t.rule.operation,callback:function(e){t.$set(t.rule,"operation",e)},expression:"rule.operation"}}):t._e()],1),t._v(" "),e("div",{staticClass:"buttons"},[t.rule.id<-1||t.dirty?e("NcButton",{on:{click:t.cancelRule}},[t._v("\n\t\t\t\t"+t._s(t.t("workflowengine","Cancel"))+"\n\t\t\t")]):t.dirty?t._e():e("NcButton",{on:{click:t.deleteRule}},[t._v("\n\t\t\t\t"+t._s(t.t("workflowengine","Delete"))+"\n\t\t\t")]),t._v(" "),e("NcButton",{attrs:{type:t.ruleStatus.type},on:{click:t.saveRule},scopedSlots:t._u([{key:"icon",fn:function(){return[e(t.ruleStatus.icon,{tag:"component",attrs:{size:20}})]},proxy:!0}],null,!1,2383918876)},[t._v("\n\t\t\t\t"+t._s(t.ruleStatus.title)+"\n\t\t\t")])],1),t._v(" "),t.error?e("p",{staticClass:"error-message"},[t._v("\n\t\t\t"+t._s(t.error)+"\n\t\t")]):t._e()],1)]):t._e()}),[],!1,null,"4bee7716",null).exports,ft=i(13299),ht=i.n(ft),gt=i(23873),Ct=i(20404);function vt(t){return vt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},vt(t)}function wt(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function bt(t){for(var e=1;e3},getMainOperations:function(){return this.showMoreOperations?Object.values(this.operations):Object.values(this.operations).slice(0,3)},showAppStoreHint:function(){return 0===this.scope&&this.appstoreEnabled&&OC.isUserAdmin()}}),mounted:function(){this.$store.dispatch("fetchRules")},methods:{createNewRule:function(t){this.$store.dispatch("createNewRule",t)}}},yt=i(7017),_t={};_t.styleTagTransform=M(),_t.setAttributes=I(),_t.insert=z().bind(null,"head"),_t.domAPI=F(),_t.insertStyleElement=$(),N()(yt.Z,_t),yt.Z&&yt.Z.locals&&yt.Z.locals;var jt=(0,Y.Z)(kt,(function(){var t=this,e=t._self._c;return e("div",{attrs:{id:"workflowengine"}},[e("NcSettingsSection",{attrs:{title:t.t("workflowengine","Available flows"),"doc-url":t.workflowDocUrl}},[0===t.scope?e("p",{staticClass:"settings-hint"},[e("a",{attrs:{href:"https://nextcloud.com/developer/"}},[t._v(t._s(t.t("workflowengine","For details on how to write your own flow, check out the development documentation.")))])]):t._e(),t._v(" "),e("transition-group",{staticClass:"actions",attrs:{name:"slide",tag:"div"}},[t._l(t.getMainOperations,(function(n){return e("Operation",{key:n.id,attrs:{operation:n},nativeOn:{click:function(e){return t.createNewRule(n)}}})})),t._v(" "),t.showAppStoreHint?e("a",{key:"add",staticClass:"actions__item colored more",attrs:{href:t.appstoreUrl}},[e("div",{staticClass:"icon icon-add"}),t._v(" "),e("div",{staticClass:"actions__item__description"},[e("h3",[t._v(t._s(t.t("workflowengine","More flows")))]),t._v(" "),e("small",[t._v(t._s(t.t("workflowengine","Browse the App Store")))])])]):t._e()],2),t._v(" "),t.hasMoreOperations?e("div",{staticClass:"actions__more"},[e("NcButton",{on:{click:function(e){t.showMoreOperations=!t.showMoreOperations}},scopedSlots:t._u([{key:"icon",fn:function(){return[t.showMoreOperations?e("MenuUp",{attrs:{size:20}}):e("MenuDown",{attrs:{size:20}})]},proxy:!0}],null,!1,3801522717)},[t._v("\n\t\t\t\t"+t._s(t.showMoreOperations?t.t("workflowengine","Show less"):t.t("workflowengine","Show more"))+"\n\t\t\t")])],1):t._e(),t._v(" "),0===t.scope?e("h2",{staticClass:"configured-flows"},[t._v("\n\t\t\t"+t._s(t.t("workflowengine","Configured flows"))+"\n\t\t")]):e("h2",{staticClass:"configured-flows"},[t._v("\n\t\t\t"+t._s(t.t("workflowengine","Your flows"))+"\n\t\t")])],1),t._v(" "),t.rules.length>0?e("transition-group",{attrs:{name:"slide"}},t._l(t.rules,(function(t){return e("Rule",{key:t.id,attrs:{rule:t}})})),1):t._e()],1)}),[],!1,null,"38a7a2e5",null).exports,Ot=/^\/(.*)\/([gui]{0,3})$/,Et=/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(3[0-2]|[1-2][0-9]|[1-9])$/,St=/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(1([01][0-9]|2[0-8])|[1-9][0-9]|[0-9])$/,Bt=i(94179),Rt=i.n(Bt),Pt={props:{value:{type:String,default:""},check:{type:Object,default:function(){return{}}}},data:function(){return{newValue:""}},watch:{value:{immediate:!0,handler:function(t){this.updateInternalValue(t)}}},methods:{updateInternalValue:function(t){this.newValue=t}}};function Vt(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=new Array(e);nt.length)&&(e=t.length);for(var n=0,i=new Array(e);nt.length)&&(e=t.length);for(var n=0,i=new Array(e);nt.length)&&(e=t.length);for(var n=0,i=new Array(e);n*[data-v-75b7ace8]:not(.close){width:180px}.check>.comparator[data-v-75b7ace8]{min-width:200px;width:200px}.check>.option[data-v-75b7ace8]{min-width:260px;width:260px;min-height:48px}.check>.option>input[type=text][data-v-75b7ace8]{min-height:48px}.check>.v-select[data-v-75b7ace8],.check>.button-vue[data-v-75b7ace8],.check>input[type=text][data-v-75b7ace8]{margin-right:5px;margin-bottom:5px}input[type=text][data-v-75b7ace8]{margin:0}.invalid[data-v-75b7ace8]{border-color:var(--color-error) !important}","",{version:3,sources:["webpack://./apps/workflowengine/src/components/Check.vue"],names:[],mappings:"AACA,wBACC,YAAA,CACA,cAAA,CACA,sBAAA,CACA,UAAA,CACA,kBAAA,CAEA,sCACC,WAAA,CAED,oCACC,eAAA,CACA,WAAA,CAED,gCACC,eAAA,CACA,WAAA,CACA,eAAA,CAEA,iDACC,eAAA,CAGF,+GAGC,gBAAA,CACA,iBAAA,CAGF,kCACC,QAAA,CAED,0BACC,0CAAA",sourcesContent:["\n.check {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start; // to not stretch components vertically\n\twidth: 100%;\n\tpadding-right: 20px;\n\n\t& > *:not(.close) {\n\t\twidth: 180px;\n\t}\n\t& > .comparator {\n\t\tmin-width: 200px;\n\t\twidth: 200px;\n\t}\n\t& > .option {\n\t\tmin-width: 260px;\n\t\twidth: 260px;\n\t\tmin-height: 48px;\n\n\t\t& > input[type=text] {\n\t\t\tmin-height: 48px;\n\t\t}\n\t}\n\t& > .v-select,\n\t& > .button-vue,\n\t& > input[type=text] {\n\t\tmargin-right: 5px;\n\t\tmargin-bottom: 5px;\n\t}\n}\ninput[type=text] {\n\tmargin: 0;\n}\n.invalid {\n\tborder-color: var(--color-error) !important;\n}\n"],sourceRoot:""}]),e.Z=a},21625:function(t,e,n){"use strict";var i=n(87537),o=n.n(i),r=n(23645),a=n.n(r)()(o());a.push([t.id,".v-select[data-v-a5701332],input[type=text][data-v-a5701332]{width:100%}input[type=text][data-v-a5701332]{min-height:48px}.option__icon[data-v-a5701332],.option__icon-img[data-v-a5701332]{display:inline-block;min-width:30px;background-position:center;vertical-align:middle}.option__icon-img[data-v-a5701332]{text-align:center}.option__title[data-v-a5701332]{display:inline-flex;width:calc(100% - 36px);vertical-align:middle}","",{version:3,sources:["webpack://./apps/workflowengine/src/components/Checks/FileMimeType.vue"],names:[],mappings:"AACA,6DAEC,UAAA,CAGD,kCACC,eAAA,CAGD,kEAEC,oBAAA,CACA,cAAA,CACA,0BAAA,CACA,qBAAA,CAGD,mCACC,iBAAA,CAGD,gCACC,mBAAA,CACA,uBAAA,CACA,qBAAA",sourcesContent:["\n.v-select,\ninput[type='text'] {\n\twidth: 100%;\n}\n\ninput[type=text] {\n\tmin-height: 48px;\n}\n\n.option__icon,\n.option__icon-img {\n\tdisplay: inline-block;\n\tmin-width: 30px;\n\tbackground-position: center;\n\tvertical-align: middle;\n}\n\n.option__icon-img {\n\ttext-align: center;\n}\n\n.option__title {\n\tdisplay: inline-flex;\n\twidth: calc(100% - 36px);\n\tvertical-align: middle;\n}\n"],sourceRoot:""}]),e.Z=a},58457:function(t,e,n){"use strict";var i=n(87537),o=n.n(i),r=n(23645),a=n.n(r)()(o());a.push([t.id,".timeslot[data-v-6efe8c39]{display:flex;flex-grow:1;flex-wrap:wrap;max-width:180px}.timeslot .multiselect[data-v-6efe8c39]{width:100%;margin-bottom:5px}.timeslot .multiselect[data-v-6efe8c39] .multiselect__tags:not(:hover):not(:focus):not(:active){border:1px solid rgba(0,0,0,0)}.timeslot input[type=text][data-v-6efe8c39]{width:50%;margin:0;margin-bottom:5px;min-height:48px}.timeslot input[type=text].timeslot--start[data-v-6efe8c39]{margin-right:5px;width:calc(50% - 5px)}.timeslot .invalid-hint[data-v-6efe8c39]{color:var(--color-text-maxcontrast)}","",{version:3,sources:["webpack://./apps/workflowengine/src/components/Checks/RequestTime.vue"],names:[],mappings:"AACA,2BACC,YAAA,CACA,WAAA,CACA,cAAA,CACA,eAAA,CAEA,wCACC,UAAA,CACA,iBAAA,CAGD,gGACC,8BAAA,CAGD,4CACC,SAAA,CACA,QAAA,CACA,iBAAA,CACA,eAAA,CAEA,4DACC,gBAAA,CACA,qBAAA,CAIF,yCACC,mCAAA",sourcesContent:["\n.timeslot {\n\tdisplay: flex;\n\tflex-grow: 1;\n\tflex-wrap: wrap;\n\tmax-width: 180px;\n\n\t.multiselect {\n\t\twidth: 100%;\n\t\tmargin-bottom: 5px;\n\t}\n\n\t.multiselect::v-deep .multiselect__tags:not(:hover):not(:focus):not(:active) {\n\t\tborder: 1px solid transparent;\n\t}\n\n\tinput[type=text] {\n\t\twidth: 50%;\n\t\tmargin: 0;\n\t\tmargin-bottom: 5px;\n\t\tmin-height: 48px;\n\n\t\t&.timeslot--start {\n\t\t\tmargin-right: 5px;\n\t\t\twidth: calc(50% - 5px);\n\t\t}\n\t}\n\n\t.invalid-hint {\n\t\tcolor: var(--color-text-maxcontrast);\n\t}\n}\n"],sourceRoot:""}]),e.Z=a},40813:function(t,e,n){"use strict";var i=n(87537),o=n.n(i),r=n(23645),a=n.n(r)()(o());a.push([t.id,".v-select[data-v-71932524],input[type=text][data-v-71932524]{width:100%}input[type=text][data-v-71932524]{min-height:48px}.option__icon[data-v-71932524]{display:inline-block;min-width:30px;background-position:center;vertical-align:middle}.option__title[data-v-71932524]{display:inline-flex;width:calc(100% - 36px);vertical-align:middle}","",{version:3,sources:["webpack://./apps/workflowengine/src/components/Checks/RequestURL.vue"],names:[],mappings:"AACA,6DAEC,UAAA,CAED,kCACC,eAAA,CAGD,+BACC,oBAAA,CACA,cAAA,CACA,0BAAA,CACA,qBAAA,CAGD,gCACC,mBAAA,CACA,uBAAA,CACA,qBAAA",sourcesContent:["\n.v-select,\ninput[type='text'] {\n\twidth: 100%;\n}\ninput[type='text'] {\n\tmin-height: 48px;\n}\n\n.option__icon {\n\tdisplay: inline-block;\n\tmin-width: 30px;\n\tbackground-position: center;\n\tvertical-align: middle;\n}\n\n.option__title {\n\tdisplay: inline-flex;\n\twidth: calc(100% - 36px);\n\tvertical-align: middle;\n}\n"],sourceRoot:""}]),e.Z=a},95773:function(t,e,n){"use strict";var i=n(87537),o=n.n(i),r=n(23645),a=n.n(r)()(o());a.push([t.id,".event[data-v-01681aaa]{margin-bottom:5px}.isComplex img[data-v-01681aaa]{vertical-align:text-top}.isComplex span[data-v-01681aaa]{padding-top:2px;display:inline-block}.multiselect[data-v-01681aaa]{width:100%;max-width:550px;margin-top:4px}.multiselect[data-v-01681aaa] .multiselect__single{display:flex}.multiselect[data-v-01681aaa]:not(.multiselect--active) .multiselect__tags{background-color:var(--color-main-background) !important;border:1px solid rgba(0,0,0,0)}.multiselect[data-v-01681aaa] .multiselect__tags{background-color:var(--color-main-background) !important;height:auto;min-height:34px}.multiselect[data-v-01681aaa]:not(.multiselect--disabled) .multiselect__tags .multiselect__single{background-image:var(--icon-triangle-s-dark);background-repeat:no-repeat;background-position:right center}input[data-v-01681aaa]{border:1px solid rgba(0,0,0,0)}.option__title[data-v-01681aaa]{margin-left:5px;color:var(--color-main-text)}.option__title_single[data-v-01681aaa]{font-weight:900}.option__icon[data-v-01681aaa]{width:16px;height:16px;filter:var(--background-invert-if-dark)}.eventlist img[data-v-01681aaa],.eventlist .text[data-v-01681aaa]{vertical-align:middle}","",{version:3,sources:["webpack://./apps/workflowengine/src/components/Event.vue"],names:[],mappings:"AACA,wBACC,iBAAA,CAGA,gCACC,uBAAA,CAED,iCACC,eAAA,CACA,oBAAA,CAGF,8BACC,UAAA,CACA,eAAA,CACA,cAAA,CAED,mDACC,YAAA,CAED,2EACC,wDAAA,CACA,8BAAA,CAGD,iDACC,wDAAA,CACA,WAAA,CACA,eAAA,CAGD,kGACC,4CAAA,CACA,2BAAA,CACA,gCAAA,CAGD,uBACC,8BAAA,CAGD,gCACC,eAAA,CACA,4BAAA,CAED,uCACC,eAAA,CAGD,+BACC,UAAA,CACA,WAAA,CACA,uCAAA,CAGD,kEAEC,qBAAA",sourcesContent:["\n.event {\n\tmargin-bottom: 5px;\n}\n.isComplex {\n\timg {\n\t\tvertical-align: text-top;\n\t}\n\tspan {\n\t\tpadding-top: 2px;\n\t\tdisplay: inline-block;\n\t}\n}\n.multiselect {\n\twidth: 100%;\n\tmax-width: 550px;\n\tmargin-top: 4px;\n}\n.multiselect::v-deep .multiselect__single {\n\tdisplay: flex;\n}\n.multiselect:not(.multiselect--active)::v-deep .multiselect__tags {\n\tbackground-color: var(--color-main-background) !important;\n\tborder: 1px solid transparent;\n}\n\n.multiselect::v-deep .multiselect__tags {\n\tbackground-color: var(--color-main-background) !important;\n\theight: auto;\n\tmin-height: 34px;\n}\n\n.multiselect:not(.multiselect--disabled)::v-deep .multiselect__tags .multiselect__single {\n\tbackground-image: var(--icon-triangle-s-dark);\n\tbackground-repeat: no-repeat;\n\tbackground-position: right center;\n}\n\ninput {\n\tborder: 1px solid transparent;\n}\n\n.option__title {\n\tmargin-left: 5px;\n\tcolor: var(--color-main-text);\n}\n.option__title_single {\n\tfont-weight: 900;\n}\n\n.option__icon {\n\twidth: 16px;\n\theight: 16px;\n\tfilter: var(--background-invert-if-dark);\n}\n\n.eventlist img,\n.eventlist .text {\n\tvertical-align: middle;\n}\n"],sourceRoot:""}]),e.Z=a},28902:function(t,e,n){"use strict";var i=n(87537),o=n.n(i),r=n(23645),a=n.n(r)()(o());a.push([t.id,".actions__item[data-v-90eae5ec]{display:flex;flex-wrap:wrap;flex-direction:column;flex-grow:1;margin-left:-1px;padding:10px;border-radius:var(--border-radius-large);margin-right:20px;margin-bottom:20px}.actions__item .icon[data-v-90eae5ec]{display:block;width:100%;height:50px;background-size:50px 50px;background-position:center center;margin-top:10px;margin-bottom:10px;background-repeat:no-repeat}.actions__item__description[data-v-90eae5ec]{text-align:center;flex-grow:1;display:flex;flex-direction:column;align-items:center}.actions__item_options[data-v-90eae5ec]{width:100%;margin-top:10px;padding-left:60px}h3[data-v-90eae5ec],small[data-v-90eae5ec]{padding:6px;display:block}h3[data-v-90eae5ec]{margin:0;padding:0;font-weight:600}small[data-v-90eae5ec]{font-size:10pt;flex-grow:1}.colored[data-v-90eae5ec]:not(.more){background-color:var(--color-primary-element)}.colored:not(.more) h3[data-v-90eae5ec],.colored:not(.more) small[data-v-90eae5ec]{color:var(--color-primary-text)}.actions__item[data-v-90eae5ec]:not(.colored){flex-direction:row}.actions__item:not(.colored) .actions__item__description[data-v-90eae5ec]{padding-top:5px;text-align:left;width:calc(100% - 105px)}.actions__item:not(.colored) .actions__item__description small[data-v-90eae5ec]{padding:0}.actions__item:not(.colored) .icon[data-v-90eae5ec]{width:50px;margin:0;margin-right:10px}.actions__item:not(.colored) .icon[data-v-90eae5ec]:not(.icon-invert){filter:var(--background-invert-if-bright)}.colored .icon-invert[data-v-90eae5ec]{filter:var(--background-invert-if-bright)}","",{version:3,sources:["webpack://./apps/workflowengine/src/styles/operation.scss"],names:[],mappings:"AAAA,gCACC,YAAA,CACA,cAAA,CACA,qBAAA,CACA,WAAA,CACA,gBAAA,CACA,YAAA,CACA,wCAAA,CACA,iBAAA,CACA,kBAAA,CAED,sCACC,aAAA,CACA,UAAA,CACA,WAAA,CACA,yBAAA,CACA,iCAAA,CACA,eAAA,CACA,kBAAA,CACA,2BAAA,CAED,6CACC,iBAAA,CACA,WAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CAED,wCACC,UAAA,CACA,eAAA,CACA,iBAAA,CAED,2CACC,WAAA,CACA,aAAA,CAED,oBACC,QAAA,CACA,SAAA,CACA,eAAA,CAED,uBACC,cAAA,CACA,WAAA,CAGD,qCACC,6CAAA,CACA,mFACC,+BAAA,CAIF,8CACC,kBAAA,CAEA,0EACC,eAAA,CACA,eAAA,CACA,wBAAA,CACA,gFACC,SAAA,CAGF,oDACC,UAAA,CACA,QAAA,CACA,iBAAA,CACA,sEACC,yCAAA,CAKH,uCACC,yCAAA",sourcesContent:[".actions__item {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tflex-direction: column;\n\tflex-grow: 1;\n\tmargin-left: -1px;\n\tpadding: 10px;\n\tborder-radius: var(--border-radius-large);\n\tmargin-right: 20px;\n\tmargin-bottom: 20px;\n}\n.actions__item .icon {\n\tdisplay: block;\n\twidth: 100%;\n\theight: 50px;\n\tbackground-size: 50px 50px;\n\tbackground-position: center center;\n\tmargin-top: 10px;\n\tmargin-bottom: 10px;\n\tbackground-repeat: no-repeat;\n}\n.actions__item__description {\n\ttext-align: center;\n\tflex-grow: 1;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n}\n.actions__item_options {\n\twidth: 100%;\n\tmargin-top: 10px;\n\tpadding-left: 60px;\n}\nh3, small {\n\tpadding: 6px;\n\tdisplay: block;\n}\nh3 {\n\tmargin: 0;\n\tpadding: 0;\n\tfont-weight: 600;\n}\nsmall {\n\tfont-size: 10pt;\n\tflex-grow: 1;\n}\n\n.colored:not(.more) {\n\tbackground-color: var(--color-primary-element);\n\th3, small {\n\t\tcolor: var(--color-primary-text)\n\t}\n}\n\n.actions__item:not(.colored) {\n\tflex-direction: row;\n\n\t.actions__item__description {\n\t\tpadding-top: 5px;\n\t\ttext-align: left;\n\t\twidth: calc(100% - 105px);\n\t\tsmall {\n\t\t\tpadding: 0;\n\t\t}\n\t}\n\t.icon {\n\t\twidth: 50px;\n\t\tmargin: 0;\n\t\tmargin-right: 10px;\n\t\t&:not(.icon-invert) {\n\t\t\tfilter: var(--background-invert-if-bright);\n\t\t}\n\t}\n}\n\n.colored .icon-invert {\n\tfilter: var(--background-invert-if-bright);\n}\n"],sourceRoot:""}]),e.Z=a},44539:function(t,e,n){"use strict";var i=n(87537),o=n.n(i),r=n(23645),a=n.n(r)()(o());a.push([t.id,".buttons[data-v-4bee7716]{display:flex;justify-content:end}.buttons button[data-v-4bee7716]{margin-left:5px}.buttons button[data-v-4bee7716]:last-child{margin-right:10px}.error-message[data-v-4bee7716]{float:right;margin-right:10px}.flow-icon[data-v-4bee7716]{width:44px}.rule[data-v-4bee7716]{display:flex;flex-wrap:wrap;border-left:5px solid var(--color-primary-element)}.rule .trigger[data-v-4bee7716],.rule .action[data-v-4bee7716]{flex-grow:1;min-height:100px;max-width:920px}.rule .action[data-v-4bee7716]{max-width:400px;position:relative}.rule .icon-confirm[data-v-4bee7716]{background-position:right 27px;padding-right:20px;margin-right:20px}.trigger p[data-v-4bee7716],.action p[data-v-4bee7716]{min-height:34px;display:flex}.trigger p>span[data-v-4bee7716],.action p>span[data-v-4bee7716]{min-width:50px;text-align:right;color:var(--color-text-maxcontrast);padding-right:10px;padding-top:6px}.trigger p .multiselect[data-v-4bee7716],.action p .multiselect[data-v-4bee7716]{flex-grow:1;max-width:300px}.trigger p:first-child span[data-v-4bee7716]{padding-top:3px}.trigger p[data-v-4bee7716]:last-child{padding-top:8px}.check--add[data-v-4bee7716]{background-position:7px center;background-color:rgba(0,0,0,0);padding-left:6px;margin:0;width:180px;border-radius:var(--border-radius);color:var(--color-text-maxcontrast);font-weight:normal;text-align:left;font-size:1em}@media(max-width: 1400px){.rule[data-v-4bee7716],.rule .trigger[data-v-4bee7716],.rule .action[data-v-4bee7716]{width:100%;max-width:100%}.rule .flow-icon[data-v-4bee7716]{display:none}}","",{version:3,sources:["webpack://./apps/workflowengine/src/components/Rule.vue"],names:[],mappings:"AAEA,0BACC,YAAA,CACA,mBAAA,CAEA,iCACC,eAAA,CAED,4CACC,iBAAA,CAIF,gCACC,WAAA,CACA,iBAAA,CAGD,4BACC,UAAA,CAGD,uBACC,YAAA,CACA,cAAA,CACA,kDAAA,CAEA,+DAEC,WAAA,CACA,gBAAA,CACA,eAAA,CAED,+BACC,eAAA,CACA,iBAAA,CAED,qCACC,8BAAA,CACA,kBAAA,CACA,iBAAA,CAGF,uDACC,eAAA,CACA,YAAA,CAEA,iEACC,cAAA,CACA,gBAAA,CACA,mCAAA,CACA,kBAAA,CACA,eAAA,CAED,iFACC,WAAA,CACA,eAAA,CAGF,6CACE,eAAA,CAEF,uCACE,eAAA,CAGF,6BACC,8BAAA,CACA,8BAAA,CACA,gBAAA,CACA,QAAA,CACA,WAAA,CACA,kCAAA,CACA,mCAAA,CACA,kBAAA,CACA,eAAA,CACA,aAAA,CAGD,0BAEE,sFACC,UAAA,CACA,cAAA,CAED,kCACC,YAAA,CAAA",sourcesContent:["\n\n.buttons {\n\tdisplay: flex;\n\tjustify-content: end;\n\n\tbutton {\n\t\tmargin-left: 5px;\n\t}\n\tbutton:last-child{\n\t\tmargin-right: 10px;\n\t}\n}\n\n.error-message {\n\tfloat: right;\n\tmargin-right: 10px;\n}\n\n.flow-icon {\n\twidth: 44px;\n}\n\n.rule {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tborder-left: 5px solid var(--color-primary-element);\n\n\t.trigger,\n\t.action {\n\t\tflex-grow: 1;\n\t\tmin-height: 100px;\n\t\tmax-width: 920px;\n\t}\n\t.action {\n\t\tmax-width: 400px;\n\t\tposition: relative;\n\t}\n\t.icon-confirm {\n\t\tbackground-position: right 27px;\n\t\tpadding-right: 20px;\n\t\tmargin-right: 20px;\n\t}\n}\n.trigger p, .action p {\n\tmin-height: 34px;\n\tdisplay: flex;\n\n\t& > span {\n\t\tmin-width: 50px;\n\t\ttext-align: right;\n\t\tcolor: var(--color-text-maxcontrast);\n\t\tpadding-right: 10px;\n\t\tpadding-top: 6px;\n\t}\n\t.multiselect {\n\t\tflex-grow: 1;\n\t\tmax-width: 300px;\n\t}\n}\n.trigger p:first-child span {\n\t\tpadding-top: 3px;\n}\n.trigger p:last-child {\n\t\tpadding-top: 8px;\n}\n\n.check--add {\n\tbackground-position: 7px center;\n\tbackground-color: transparent;\n\tpadding-left: 6px;\n\tmargin: 0;\n\twidth: 180px;\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: normal;\n\ttext-align: left;\n\tfont-size: 1em;\n}\n\n@media (max-width:1400px) {\n\t.rule {\n\t\t&, .trigger, .action {\n\t\t\twidth: 100%;\n\t\t\tmax-width: 100%;\n\t\t}\n\t\t.flow-icon {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n}\n\n"],sourceRoot:""}]),e.Z=a},7017:function(t,e,n){"use strict";var i=n(87537),o=n.n(i),r=n(23645),a=n.n(r)()(o());a.push([t.id,"#workflowengine[data-v-38a7a2e5]{border-bottom:1px solid var(--color-border)}.section[data-v-38a7a2e5]{max-width:100vw}.section h2.configured-flows[data-v-38a7a2e5]{margin-top:50px;margin-bottom:0}.actions[data-v-38a7a2e5]{display:flex;flex-wrap:wrap;max-width:1200px}.actions .actions__item[data-v-38a7a2e5]{max-width:280px;flex-basis:250px}.actions__more[data-v-38a7a2e5]{margin-bottom:10px}.slide-enter-active[data-v-38a7a2e5]{-moz-transition-duration:.3s;-webkit-transition-duration:.3s;-o-transition-duration:.3s;transition-duration:.3s;-moz-transition-timing-function:ease-in;-webkit-transition-timing-function:ease-in;-o-transition-timing-function:ease-in;transition-timing-function:ease-in}.slide-leave-active[data-v-38a7a2e5]{-moz-transition-duration:.3s;-webkit-transition-duration:.3s;-o-transition-duration:.3s;transition-duration:.3s;-moz-transition-timing-function:cubic-bezier(0, 1, 0.5, 1);-webkit-transition-timing-function:cubic-bezier(0, 1, 0.5, 1);-o-transition-timing-function:cubic-bezier(0, 1, 0.5, 1);transition-timing-function:cubic-bezier(0, 1, 0.5, 1)}.slide-enter-to[data-v-38a7a2e5],.slide-leave[data-v-38a7a2e5]{max-height:500px;overflow:hidden}.slide-enter[data-v-38a7a2e5],.slide-leave-to[data-v-38a7a2e5]{overflow:hidden;max-height:0;padding-top:0;padding-bottom:0}.actions__item[data-v-38a7a2e5]{display:flex;flex-wrap:wrap;flex-direction:column;flex-grow:1;margin-left:-1px;padding:10px;border-radius:var(--border-radius-large);margin-right:20px;margin-bottom:20px}.actions__item .icon[data-v-38a7a2e5]{display:block;width:100%;height:50px;background-size:50px 50px;background-position:center center;margin-top:10px;margin-bottom:10px;background-repeat:no-repeat}.actions__item__description[data-v-38a7a2e5]{text-align:center;flex-grow:1;display:flex;flex-direction:column;align-items:center}.actions__item_options[data-v-38a7a2e5]{width:100%;margin-top:10px;padding-left:60px}h3[data-v-38a7a2e5],small[data-v-38a7a2e5]{padding:6px;display:block}h3[data-v-38a7a2e5]{margin:0;padding:0;font-weight:600}small[data-v-38a7a2e5]{font-size:10pt;flex-grow:1}.colored[data-v-38a7a2e5]:not(.more){background-color:var(--color-primary-element)}.colored:not(.more) h3[data-v-38a7a2e5],.colored:not(.more) small[data-v-38a7a2e5]{color:var(--color-primary-text)}.actions__item[data-v-38a7a2e5]:not(.colored){flex-direction:row}.actions__item:not(.colored) .actions__item__description[data-v-38a7a2e5]{padding-top:5px;text-align:left;width:calc(100% - 105px)}.actions__item:not(.colored) .actions__item__description small[data-v-38a7a2e5]{padding:0}.actions__item:not(.colored) .icon[data-v-38a7a2e5]{width:50px;margin:0;margin-right:10px}.actions__item:not(.colored) .icon[data-v-38a7a2e5]:not(.icon-invert){filter:var(--background-invert-if-bright)}.colored .icon-invert[data-v-38a7a2e5]{filter:var(--background-invert-if-bright)}.actions__item.more[data-v-38a7a2e5]{background-color:var(--color-background-dark)}","",{version:3,sources:["webpack://./apps/workflowengine/src/components/Workflow.vue","webpack://./apps/workflowengine/src/styles/operation.scss"],names:[],mappings:"AACA,iCACC,2CAAA,CAED,0BACC,eAAA,CAEA,8CACC,eAAA,CACA,eAAA,CAGF,0BACC,YAAA,CACA,cAAA,CACA,gBAAA,CACA,yCACC,eAAA,CACA,gBAAA,CAGF,gCACC,kBAAA,CAGD,qCACC,4BAAA,CACA,+BAAA,CACA,0BAAA,CACA,uBAAA,CACA,uCAAA,CACA,0CAAA,CACA,qCAAA,CACA,kCAAA,CAGD,qCACC,4BAAA,CACA,+BAAA,CACA,0BAAA,CACA,uBAAA,CACA,0DAAA,CACA,6DAAA,CACA,wDAAA,CACA,qDAAA,CAGD,+DACC,gBAAA,CACA,eAAA,CAGD,+DACC,eAAA,CACA,YAAA,CACA,aAAA,CACA,gBAAA,CCxDD,gCACC,YAAA,CACA,cAAA,CACA,qBAAA,CACA,WAAA,CACA,gBAAA,CACA,YAAA,CACA,wCAAA,CACA,iBAAA,CACA,kBAAA,CAED,sCACC,aAAA,CACA,UAAA,CACA,WAAA,CACA,yBAAA,CACA,iCAAA,CACA,eAAA,CACA,kBAAA,CACA,2BAAA,CAED,6CACC,iBAAA,CACA,WAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CAED,wCACC,UAAA,CACA,eAAA,CACA,iBAAA,CAED,2CACC,WAAA,CACA,aAAA,CAED,oBACC,QAAA,CACA,SAAA,CACA,eAAA,CAED,uBACC,cAAA,CACA,WAAA,CAGD,qCACC,6CAAA,CACA,mFACC,+BAAA,CAIF,8CACC,kBAAA,CAEA,0EACC,eAAA,CACA,eAAA,CACA,wBAAA,CACA,gFACC,SAAA,CAGF,oDACC,UAAA,CACA,QAAA,CACA,iBAAA,CACA,sEACC,yCAAA,CAKH,uCACC,yCAAA,CDfD,qCACC,6CAAA",sourcesContent:['\n#workflowengine {\n\tborder-bottom: 1px solid var(--color-border);\n}\n.section {\n\tmax-width: 100vw;\n\n\th2.configured-flows {\n\t\tmargin-top: 50px;\n\t\tmargin-bottom: 0;\n\t}\n}\n.actions {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tmax-width: 1200px;\n\t.actions__item {\n\t\tmax-width: 280px;\n\t\tflex-basis: 250px;\n\t}\n}\n.actions__more {\n\tmargin-bottom: 10px;\n}\n\n.slide-enter-active {\n\t-moz-transition-duration: 0.3s;\n\t-webkit-transition-duration: 0.3s;\n\t-o-transition-duration: 0.3s;\n\ttransition-duration: 0.3s;\n\t-moz-transition-timing-function: ease-in;\n\t-webkit-transition-timing-function: ease-in;\n\t-o-transition-timing-function: ease-in;\n\ttransition-timing-function: ease-in;\n}\n\n.slide-leave-active {\n\t-moz-transition-duration: 0.3s;\n\t-webkit-transition-duration: 0.3s;\n\t-o-transition-duration: 0.3s;\n\ttransition-duration: 0.3s;\n\t-moz-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\n\t-webkit-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\n\t-o-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\n\ttransition-timing-function: cubic-bezier(0, 1, 0.5, 1);\n}\n\n.slide-enter-to, .slide-leave {\n\tmax-height: 500px;\n\toverflow: hidden;\n}\n\n.slide-enter, .slide-leave-to {\n\toverflow: hidden;\n\tmax-height: 0;\n\tpadding-top: 0;\n\tpadding-bottom: 0;\n}\n\n@import "./../styles/operation";\n\n.actions__item.more {\n\tbackground-color: var(--color-background-dark);\n}\n',".actions__item {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tflex-direction: column;\n\tflex-grow: 1;\n\tmargin-left: -1px;\n\tpadding: 10px;\n\tborder-radius: var(--border-radius-large);\n\tmargin-right: 20px;\n\tmargin-bottom: 20px;\n}\n.actions__item .icon {\n\tdisplay: block;\n\twidth: 100%;\n\theight: 50px;\n\tbackground-size: 50px 50px;\n\tbackground-position: center center;\n\tmargin-top: 10px;\n\tmargin-bottom: 10px;\n\tbackground-repeat: no-repeat;\n}\n.actions__item__description {\n\ttext-align: center;\n\tflex-grow: 1;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n}\n.actions__item_options {\n\twidth: 100%;\n\tmargin-top: 10px;\n\tpadding-left: 60px;\n}\nh3, small {\n\tpadding: 6px;\n\tdisplay: block;\n}\nh3 {\n\tmargin: 0;\n\tpadding: 0;\n\tfont-weight: 600;\n}\nsmall {\n\tfont-size: 10pt;\n\tflex-grow: 1;\n}\n\n.colored:not(.more) {\n\tbackground-color: var(--color-primary-element);\n\th3, small {\n\t\tcolor: var(--color-primary-text)\n\t}\n}\n\n.actions__item:not(.colored) {\n\tflex-direction: row;\n\n\t.actions__item__description {\n\t\tpadding-top: 5px;\n\t\ttext-align: left;\n\t\twidth: calc(100% - 105px);\n\t\tsmall {\n\t\t\tpadding: 0;\n\t\t}\n\t}\n\t.icon {\n\t\twidth: 50px;\n\t\tmargin: 0;\n\t\tmargin-right: 10px;\n\t\t&:not(.icon-invert) {\n\t\t\tfilter: var(--background-invert-if-bright);\n\t\t}\n\t}\n}\n\n.colored .icon-invert {\n\tfilter: var(--background-invert-if-bright);\n}\n"],sourceRoot:""}]),e.Z=a},45775:function(t,e,n){"use strict";var i=n(87537),o=n.n(i),r=n(23645),a=n.n(r)()(o());a.push([t.id,"\n.v-select[data-v-6769046e],\ninput[type='text'][data-v-6769046e] {\n\twidth: 100%;\n}\ninput[type='text'][data-v-6769046e] {\n\tmin-height: 48px;\n}\n.option__icon[data-v-6769046e] {\n\tdisplay: inline-block;\n\tmin-width: 30px;\n\tbackground-position: center;\n\tvertical-align: middle;\n}\n.option__title[data-v-6769046e] {\n\tdisplay: inline-flex;\n\twidth: calc(100% - 36px);\n\tvertical-align: middle;\n}\n","",{version:3,sources:["webpack://./apps/workflowengine/src/components/Checks/RequestUserAgent.vue"],names:[],mappings:";AAmJA;;CAEA,WAAA;AACA;AACA;CACA,gBAAA;AACA;AAEA;CACA,qBAAA;CACA,eAAA;CACA,2BAAA;CACA,sBAAA;AACA;AAEA;CACA,oBAAA;CACA,wBAAA;CACA,sBAAA;AACA",sourcesContent:["\x3c!--\n - @copyright Copyright (c) 2019 Julius Härtl \n -\n - @author Julius Härtl \n -\n - @license GNU AGPL version 3 or any later version\n -\n - This program is free software: you can redistribute it and/or modify\n - it under the terms of the GNU Affero General Public License as\n - published by the Free Software Foundation, either version 3 of the\n - License, or (at your option) any later version.\n -\n - This program is distributed in the hope that it will be useful,\n - but WITHOUT ANY WARRANTY; without even the implied warranty of\n - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n - GNU Affero General Public License for more details.\n -\n - You should have received a copy of the GNU Affero General Public License\n - along with this program. If not, see .\n -\n --\x3e\n\n\n\n\n\n\n","\n import API from \"!../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Event.vue?vue&type=style&index=0&id=01681aaa&prod&scoped=true&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Event.vue?vue&type=style&index=0&id=01681aaa&prod&scoped=true&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./Event.vue?vue&type=template&id=01681aaa&scoped=true&\"\nimport script from \"./Event.vue?vue&type=script&lang=js&\"\nexport * from \"./Event.vue?vue&type=script&lang=js&\"\nimport style0 from \"./Event.vue?vue&type=style&index=0&id=01681aaa&prod&scoped=true&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"01681aaa\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',{staticClass:\"event\"},[(_vm.operation.isComplex && _vm.operation.fixedEntity !== '')?_c('div',{staticClass:\"isComplex\"},[_c('img',{staticClass:\"option__icon\",attrs:{\"src\":_vm.entity.icon,\"alt\":\"\"}}),_vm._v(\" \"),_c('span',{staticClass:\"option__title option__title_single\"},[_vm._v(_vm._s(_vm.operation.triggerHint))])]):_c('NcMultiselect',{attrs:{\"value\":_vm.currentEvent,\"options\":_vm.allEvents,\"track-by\":\"id\",\"multiple\":true,\"auto-limit\":false,\"disabled\":_vm.allEvents.length <= 1},on:{\"input\":_vm.updateEvent},scopedSlots:_vm._u([{key:\"selection\",fn:function({ values, isOpen }){return [(values.length && !isOpen)?_c('div',{staticClass:\"eventlist\"},[_c('img',{staticClass:\"option__icon\",attrs:{\"src\":values[0].entity.icon,\"alt\":\"\"}}),_vm._v(\" \"),_vm._l((values),function(value,index){return _c('span',{key:value.id,staticClass:\"text option__title option__title_single\"},[_vm._v(_vm._s(value.displayName)+\" \"),(index+1 < values.length)?_c('span',[_vm._v(\", \")]):_vm._e()])})],2):_vm._e()]}},{key:\"option\",fn:function(props){return [_c('img',{staticClass:\"option__icon\",attrs:{\"src\":props.option.entity.icon,\"alt\":\"\"}}),_vm._v(\" \"),_c('span',{staticClass:\"option__title\"},[_vm._v(_vm._s(props.option.displayName))])]}}])})],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Check.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Check.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","\n import API from \"!../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Check.vue?vue&type=style&index=0&id=75b7ace8&prod&scoped=true&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Check.vue?vue&type=style&index=0&id=75b7ace8&prod&scoped=true&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./Check.vue?vue&type=template&id=75b7ace8&scoped=true&\"\nimport script from \"./Check.vue?vue&type=script&lang=js&\"\nexport * from \"./Check.vue?vue&type=script&lang=js&\"\nimport style0 from \"./Check.vue?vue&type=style&index=0&id=75b7ace8&prod&scoped=true&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"75b7ace8\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',{directives:[{name:\"click-outside\",rawName:\"v-click-outside\",value:(_vm.hideDelete),expression:\"hideDelete\"}],staticClass:\"check\",on:{\"click\":_vm.showDelete}},[_c('NcSelect',{ref:\"checkSelector\",attrs:{\"options\":_vm.options,\"label\":\"name\",\"track-by\":\"class\",\"clearable\":false,\"placeholder\":_vm.t('workflowengine', 'Select a filter')},on:{\"input\":_vm.updateCheck},model:{value:(_vm.currentOption),callback:function ($$v) {_vm.currentOption=$$v},expression:\"currentOption\"}}),_vm._v(\" \"),_c('NcSelect',{staticClass:\"comparator\",attrs:{\"disabled\":!_vm.currentOption,\"options\":_vm.operators,\"label\":\"name\",\"track-by\":\"operator\",\"clearable\":false,\"placeholder\":_vm.t('workflowengine', 'Select a comparator')},on:{\"input\":_vm.updateCheck},model:{value:(_vm.currentOperator),callback:function ($$v) {_vm.currentOperator=$$v},expression:\"currentOperator\"}}),_vm._v(\" \"),(_vm.currentOperator && _vm.currentComponent)?_c(_vm.currentOption.component,{tag:\"component\",staticClass:\"option\",attrs:{\"disabled\":!_vm.currentOption,\"check\":_vm.check},on:{\"input\":_vm.updateCheck,\"valid\":function($event){(_vm.valid=true) && _vm.validate()},\"invalid\":function($event){!(_vm.valid=false) && _vm.validate()}},model:{value:(_vm.check.value),callback:function ($$v) {_vm.$set(_vm.check, \"value\", $$v)},expression:\"check.value\"}}):_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.check.value),expression:\"check.value\"}],staticClass:\"option\",class:{ invalid: !_vm.valid },attrs:{\"type\":\"text\",\"disabled\":!_vm.currentOption,\"placeholder\":_vm.valuePlaceholder},domProps:{\"value\":(_vm.check.value)},on:{\"input\":[function($event){if($event.target.composing)return;_vm.$set(_vm.check, \"value\", $event.target.value)},_vm.updateCheck]}}),_vm._v(\" \"),(_vm.deleteVisible || !_vm.currentOption)?_c('NcActions',[_c('NcActionButton',{attrs:{\"title\":_vm.t('workflowengine', 'Remove filter')},on:{\"click\":function($event){return _vm.$emit('remove')}},scopedSlots:_vm._u([{key:\"icon\",fn:function(){return [_c('CloseIcon',{attrs:{\"size\":20}})]},proxy:true}],null,false,2428343285)})],1):_vm._e()],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Operation.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Operation.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","\n import API from \"!../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Operation.vue?vue&type=style&index=0&id=90eae5ec&prod&scoped=true&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Operation.vue?vue&type=style&index=0&id=90eae5ec&prod&scoped=true&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./Operation.vue?vue&type=template&id=90eae5ec&scoped=true&\"\nimport script from \"./Operation.vue?vue&type=script&lang=js&\"\nexport * from \"./Operation.vue?vue&type=script&lang=js&\"\nimport style0 from \"./Operation.vue?vue&type=style&index=0&id=90eae5ec&prod&scoped=true&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"90eae5ec\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',{staticClass:\"actions__item\",class:{'colored': _vm.colored},style:({ backgroundColor: _vm.colored ? _vm.operation.color : 'transparent' })},[_c('div',{staticClass:\"icon\",class:_vm.operation.iconClass,style:({ backgroundImage: _vm.operation.iconClass ? '' : `url(${_vm.operation.icon})` })}),_vm._v(\" \"),_c('div',{staticClass:\"actions__item__description\"},[_c('h3',[_vm._v(_vm._s(_vm.operation.name))]),_vm._v(\" \"),_c('small',[_vm._v(_vm._s(_vm.operation.description))]),_vm._v(\" \"),(_vm.colored)?_c('NcButton',[_vm._v(\"\\n\\t\\t\\t\"+_vm._s(_vm.t('workflowengine', 'Add new flow'))+\"\\n\\t\\t\")]):_vm._e()],1),_vm._v(\" \"),_c('div',{staticClass:\"actions__item_options\"},[_vm._t(\"default\")],2)])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Rule.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Rule.vue?vue&type=script&lang=js&\"","\n import API from \"!../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Rule.vue?vue&type=style&index=0&id=4bee7716&prod&scoped=true&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Rule.vue?vue&type=style&index=0&id=4bee7716&prod&scoped=true&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./Rule.vue?vue&type=template&id=4bee7716&scoped=true&\"\nimport script from \"./Rule.vue?vue&type=script&lang=js&\"\nexport * from \"./Rule.vue?vue&type=script&lang=js&\"\nimport style0 from \"./Rule.vue?vue&type=style&index=0&id=4bee7716&prod&scoped=true&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"4bee7716\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return (_vm.operation)?_c('div',{staticClass:\"section rule\",style:({ borderLeftColor: _vm.operation.color || '' })},[_c('div',{staticClass:\"trigger\"},[_c('p',[_c('span',[_vm._v(_vm._s(_vm.t('workflowengine', 'When')))]),_vm._v(\" \"),_c('Event',{attrs:{\"rule\":_vm.rule},on:{\"update\":_vm.updateRule}})],1),_vm._v(\" \"),_vm._l((_vm.rule.checks),function(check,index){return _c('p',{key:index},[_c('span',[_vm._v(_vm._s(_vm.t('workflowengine', 'and')))]),_vm._v(\" \"),_c('Check',{attrs:{\"check\":check,\"rule\":_vm.rule},on:{\"update\":_vm.updateRule,\"validate\":_vm.validate,\"remove\":function($event){return _vm.removeCheck(check)}}})],1)}),_vm._v(\" \"),_c('p',[_c('span'),_vm._v(\" \"),(_vm.lastCheckComplete)?_c('input',{staticClass:\"check--add\",attrs:{\"type\":\"button\",\"value\":_vm.t('workflowengine', 'Add a new filter')},on:{\"click\":_vm.onAddFilter}}):_vm._e()])],2),_vm._v(\" \"),_c('div',{staticClass:\"flow-icon icon-confirm\"}),_vm._v(\" \"),_c('div',{staticClass:\"action\"},[_c('Operation',{attrs:{\"operation\":_vm.operation,\"colored\":false}},[(_vm.operation.options)?_c(_vm.operation.options,{tag:\"component\",on:{\"input\":_vm.updateOperation},model:{value:(_vm.rule.operation),callback:function ($$v) {_vm.$set(_vm.rule, \"operation\", $$v)},expression:\"rule.operation\"}}):_vm._e()],1),_vm._v(\" \"),_c('div',{staticClass:\"buttons\"},[(_vm.rule.id < -1 || _vm.dirty)?_c('NcButton',{on:{\"click\":_vm.cancelRule}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.t('workflowengine', 'Cancel'))+\"\\n\\t\\t\\t\")]):(!_vm.dirty)?_c('NcButton',{on:{\"click\":_vm.deleteRule}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.t('workflowengine', 'Delete'))+\"\\n\\t\\t\\t\")]):_vm._e(),_vm._v(\" \"),_c('NcButton',{attrs:{\"type\":_vm.ruleStatus.type},on:{\"click\":_vm.saveRule},scopedSlots:_vm._u([{key:\"icon\",fn:function(){return [_c(_vm.ruleStatus.icon,{tag:\"component\",attrs:{\"size\":20}})]},proxy:true}],null,false,2383918876)},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.ruleStatus.title)+\"\\n\\t\\t\\t\")])],1),_vm._v(\" \"),(_vm.error)?_c('p',{staticClass:\"error-message\"},[_vm._v(\"\\n\\t\\t\\t\"+_vm._s(_vm.error)+\"\\n\\t\\t\")]):_vm._e()],1)]):_vm._e()\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Workflow.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Workflow.vue?vue&type=script&lang=js&\"","\n import API from \"!../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Workflow.vue?vue&type=style&index=0&id=38a7a2e5&prod&scoped=true&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../node_modules/css-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../node_modules/sass-loader/dist/cjs.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Workflow.vue?vue&type=style&index=0&id=38a7a2e5&prod&scoped=true&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./Workflow.vue?vue&type=template&id=38a7a2e5&scoped=true&\"\nimport script from \"./Workflow.vue?vue&type=script&lang=js&\"\nexport * from \"./Workflow.vue?vue&type=script&lang=js&\"\nimport style0 from \"./Workflow.vue?vue&type=style&index=0&id=38a7a2e5&prod&scoped=true&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"38a7a2e5\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',{attrs:{\"id\":\"workflowengine\"}},[_c('NcSettingsSection',{attrs:{\"title\":_vm.t('workflowengine', 'Available flows'),\"doc-url\":_vm.workflowDocUrl}},[(_vm.scope === 0)?_c('p',{staticClass:\"settings-hint\"},[_c('a',{attrs:{\"href\":\"https://nextcloud.com/developer/\"}},[_vm._v(_vm._s(_vm.t('workflowengine', 'For details on how to write your own flow, check out the development documentation.')))])]):_vm._e(),_vm._v(\" \"),_c('transition-group',{staticClass:\"actions\",attrs:{\"name\":\"slide\",\"tag\":\"div\"}},[_vm._l((_vm.getMainOperations),function(operation){return _c('Operation',{key:operation.id,attrs:{\"operation\":operation},nativeOn:{\"click\":function($event){return _vm.createNewRule(operation)}}})}),_vm._v(\" \"),(_vm.showAppStoreHint)?_c('a',{key:'add',staticClass:\"actions__item colored more\",attrs:{\"href\":_vm.appstoreUrl}},[_c('div',{staticClass:\"icon icon-add\"}),_vm._v(\" \"),_c('div',{staticClass:\"actions__item__description\"},[_c('h3',[_vm._v(_vm._s(_vm.t('workflowengine', 'More flows')))]),_vm._v(\" \"),_c('small',[_vm._v(_vm._s(_vm.t('workflowengine', 'Browse the App Store')))])])]):_vm._e()],2),_vm._v(\" \"),(_vm.hasMoreOperations)?_c('div',{staticClass:\"actions__more\"},[_c('NcButton',{on:{\"click\":function($event){_vm.showMoreOperations = !_vm.showMoreOperations}},scopedSlots:_vm._u([{key:\"icon\",fn:function(){return [(_vm.showMoreOperations)?_c('MenuUp',{attrs:{\"size\":20}}):_c('MenuDown',{attrs:{\"size\":20}})]},proxy:true}],null,false,3801522717)},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.showMoreOperations ? _vm.t('workflowengine', 'Show less') : _vm.t('workflowengine', 'Show more'))+\"\\n\\t\\t\\t\")])],1):_vm._e(),_vm._v(\" \"),(_vm.scope === 0)?_c('h2',{staticClass:\"configured-flows\"},[_vm._v(\"\\n\\t\\t\\t\"+_vm._s(_vm.t('workflowengine', 'Configured flows'))+\"\\n\\t\\t\")]):_c('h2',{staticClass:\"configured-flows\"},[_vm._v(\"\\n\\t\\t\\t\"+_vm._s(_vm.t('workflowengine', 'Your flows'))+\"\\n\\t\\t\")])],1),_vm._v(\" \"),(_vm.rules.length > 0)?_c('transition-group',{attrs:{\"name\":\"slide\"}},_vm._l((_vm.rules),function(rule){return _c('Rule',{key:rule.id,attrs:{\"rule\":rule}})}),1):_vm._e()],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","/**\n * @copyright Copyright (c) 2019 Julius Härtl \n *\n * @author Julius Härtl \n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\nconst regexRegex = /^\\/(.*)\\/([gui]{0,3})$/\nconst regexIPv4 = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\/(3[0-2]|[1-2][0-9]|[1-9])$/\nconst regexIPv6 = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/(1([01][0-9]|2[0-8])|[1-9][0-9]|[0-9])$/\n\nconst validateRegex = function(string) {\n\tif (!string) {\n\t\treturn false\n\t}\n\treturn regexRegex.exec(string) !== null\n}\n\nconst validateIPv4 = function(string) {\n\tif (!string) {\n\t\treturn false\n\t}\n\treturn regexIPv4.exec(string) !== null\n}\n\nconst validateIPv6 = function(string) {\n\tif (!string) {\n\t\treturn false\n\t}\n\treturn regexIPv6.exec(string) !== null\n}\n\nconst stringValidator = (check) => {\n\tif (check.operator === 'matches' || check.operator === '!matches') {\n\t\treturn validateRegex(check.value)\n\t}\n\treturn true\n}\n\nexport { validateRegex, stringValidator, validateIPv4, validateIPv6 }\n","/**\n * @copyright Copyright (c) 2019 Julius Härtl \n *\n * @author John Molakvoæ \n * @author Julius Härtl \n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\nconst valueMixin = {\n\tprops: {\n\t\tvalue: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t\tcheck: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => { return {} },\n\t\t},\n\t},\n\tdata() {\n\t\treturn {\n\t\t\tnewValue: '',\n\t\t}\n\t},\n\twatch: {\n\t\tvalue: {\n\t\t\timmediate: true,\n\t\t\thandler(value) {\n\t\t\t\tthis.updateInternalValue(value)\n\t\t\t},\n\t\t},\n\t},\n\tmethods: {\n\t\tupdateInternalValue(value) {\n\t\t\tthis.newValue = value\n\t\t},\n\t},\n}\n\nexport default valueMixin\n","\n\n\n\n\n\n","import mod from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./FileMimeType.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./FileMimeType.vue?vue&type=script&lang=js&\"","\n import API from \"!../../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./FileMimeType.vue?vue&type=style&index=0&id=a5701332&prod&scoped=true&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./FileMimeType.vue?vue&type=style&index=0&id=a5701332&prod&scoped=true&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./FileMimeType.vue?vue&type=template&id=a5701332&scoped=true&\"\nimport script from \"./FileMimeType.vue?vue&type=script&lang=js&\"\nexport * from \"./FileMimeType.vue?vue&type=script&lang=js&\"\nimport style0 from \"./FileMimeType.vue?vue&type=style&index=0&id=a5701332&prod&scoped=true&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"a5701332\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',[_c('NcSelect',{attrs:{\"value\":_vm.currentValue,\"placeholder\":_vm.t('workflowengine', 'Select a file type'),\"label\":\"label\",\"options\":_vm.options,\"clearable\":false},on:{\"input\":_vm.setValue},scopedSlots:_vm._u([{key:\"option\",fn:function(option){return [(option.icon)?_c('span',{staticClass:\"option__icon\",class:option.icon}):_c('span',{staticClass:\"option__icon-img\"},[_c('img',{attrs:{\"src\":option.iconUrl,\"alt\":\"\"}})]),_vm._v(\" \"),_c('span',{staticClass:\"option__title\"},[_c('NcEllipsisedOption',{attrs:{\"name\":String(option.label)}})],1)]}},{key:\"selected-option\",fn:function(selectedOption){return [(selectedOption.icon)?_c('span',{staticClass:\"option__icon\",class:selectedOption.icon}):_c('span',{staticClass:\"option__icon-img\"},[_c('img',{attrs:{\"src\":selectedOption.iconUrl,\"alt\":\"\"}})]),_vm._v(\" \"),_c('span',{staticClass:\"option__title\"},[_c('NcEllipsisedOption',{attrs:{\"name\":String(selectedOption.label)}})],1)]}}])}),_vm._v(\" \"),(!_vm.isPredefined)?_c('input',{attrs:{\"type\":\"text\",\"placeholder\":_vm.t('workflowengine', 'e.g. httpd/unix-directory')},domProps:{\"value\":_vm.currentValue.id},on:{\"input\":_vm.updateCustom}}):_vm._e()],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./FileSystemTag.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./FileSystemTag.vue?vue&type=script&lang=js&\"","\n\n\n\n\n\n\n","import { render, staticRenderFns } from \"./FileSystemTag.vue?vue&type=template&id=3bb09106&scoped=true&\"\nimport script from \"./FileSystemTag.vue?vue&type=script&lang=js&\"\nexport * from \"./FileSystemTag.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"3bb09106\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('NcSelectTags',{attrs:{\"multiple\":false},on:{\"input\":_vm.update},model:{value:(_vm.newValue),callback:function ($$v) {_vm.newValue=$$v},expression:\"newValue\"}})\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { render, staticRenderFns } from \"./MfaVerifiedValue.vue?vue&type=template&id=db945394&\"\nvar script = {}\n\n\n/* normalize component */\nimport normalizer from \"!../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div')\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","/**\n * @copyright Copyright (c) 2019 Julius Härtl \n *\n * @author Arthur Schiwon \n * @author Julius Härtl \n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\nimport { stringValidator, validateIPv4, validateIPv6 } from '../../helpers/validators'\nimport FileMimeType from './FileMimeType'\nimport FileSystemTag from './FileSystemTag'\nimport MfaVerifiedValue from './MfaVerifiedValue.vue'\n\nconst stringOrRegexOperators = () => {\n\treturn [\n\t\t{ operator: 'matches', name: t('workflowengine', 'matches') },\n\t\t{ operator: '!matches', name: t('workflowengine', 'does not match') },\n\t\t{ operator: 'is', name: t('workflowengine', 'is') },\n\t\t{ operator: '!is', name: t('workflowengine', 'is not') },\n\t]\n}\n\nconst FileChecks = [\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\FileName',\n\t\tname: t('workflowengine', 'File name'),\n\t\toperators: stringOrRegexOperators,\n\t\tplaceholder: (check) => {\n\t\t\tif (check.operator === 'matches' || check.operator === '!matches') {\n\t\t\t\treturn '/^dummy-.+$/i'\n\t\t\t}\n\t\t\treturn 'filename.txt'\n\t\t},\n\t\tvalidate: stringValidator,\n\t},\n\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\FileMimeType',\n\t\tname: t('workflowengine', 'File MIME type'),\n\t\toperators: stringOrRegexOperators,\n\t\tcomponent: FileMimeType,\n\t},\n\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\FileSize',\n\t\tname: t('workflowengine', 'File size (upload)'),\n\t\toperators: [\n\t\t\t{ operator: 'less', name: t('workflowengine', 'less') },\n\t\t\t{ operator: '!greater', name: t('workflowengine', 'less or equals') },\n\t\t\t{ operator: '!less', name: t('workflowengine', 'greater or equals') },\n\t\t\t{ operator: 'greater', name: t('workflowengine', 'greater') },\n\t\t],\n\t\tplaceholder: (check) => '5 MB',\n\t\tvalidate: (check) => check.value ? check.value.match(/^[0-9]+[ ]?[kmgt]?b$/i) !== null : false,\n\t},\n\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\RequestRemoteAddress',\n\t\tname: t('workflowengine', 'Request remote address'),\n\t\toperators: [\n\t\t\t{ operator: 'matchesIPv4', name: t('workflowengine', 'matches IPv4') },\n\t\t\t{ operator: '!matchesIPv4', name: t('workflowengine', 'does not match IPv4') },\n\t\t\t{ operator: 'matchesIPv6', name: t('workflowengine', 'matches IPv6') },\n\t\t\t{ operator: '!matchesIPv6', name: t('workflowengine', 'does not match IPv6') },\n\t\t],\n\t\tplaceholder: (check) => {\n\t\t\tif (check.operator === 'matchesIPv6' || check.operator === '!matchesIPv6') {\n\t\t\t\treturn '::1/128'\n\t\t\t}\n\t\t\treturn '127.0.0.1/32'\n\t\t},\n\t\tvalidate: (check) => {\n\t\t\tif (check.operator === 'matchesIPv6' || check.operator === '!matchesIPv6') {\n\t\t\t\treturn validateIPv6(check.value)\n\t\t\t}\n\t\t\treturn validateIPv4(check.value)\n\t\t},\n\t},\n\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\FileSystemTags',\n\t\tname: t('workflowengine', 'File system tag'),\n\t\toperators: [\n\t\t\t{ operator: 'is', name: t('workflowengine', 'is tagged with') },\n\t\t\t{ operator: '!is', name: t('workflowengine', 'is not tagged with') },\n\t\t],\n\t\tcomponent: FileSystemTag,\n\t},\n\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\MfaVerified',\n\t\tname: t('workflowengine', 'multi-factor authentication'),\n\t\toperators: [\n\t\t\t{ operator: 'is', name: t('workflowengine', 'is verified') },\n\t\t\t{ operator: '!is', name: t('workflowengine', 'is not verified') },\n\t\t],\n\t\tcomponent: MfaVerifiedValue,\n\t},\n]\n\nexport default FileChecks\n","\n\n\n\n\n\n","import mod from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestUserAgent.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestUserAgent.vue?vue&type=script&lang=js&\"","\n import API from \"!../../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestUserAgent.vue?vue&type=style&index=0&id=6769046e&prod&scoped=true&lang=css&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestUserAgent.vue?vue&type=style&index=0&id=6769046e&prod&scoped=true&lang=css&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./RequestUserAgent.vue?vue&type=template&id=6769046e&scoped=true&\"\nimport script from \"./RequestUserAgent.vue?vue&type=script&lang=js&\"\nexport * from \"./RequestUserAgent.vue?vue&type=script&lang=js&\"\nimport style0 from \"./RequestUserAgent.vue?vue&type=style&index=0&id=6769046e&prod&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"6769046e\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',[_c('NcSelect',{attrs:{\"value\":_vm.currentValue,\"placeholder\":_vm.t('workflowengine', 'Select a user agent'),\"label\":\"label\",\"options\":_vm.options,\"clearable\":false},on:{\"input\":_vm.setValue},scopedSlots:_vm._u([{key:\"option\",fn:function(option){return [_c('span',{staticClass:\"option__icon\",class:option.icon}),_vm._v(\" \"),_c('span',{staticClass:\"option__title\"},[_c('NcEllipsisedOption',{attrs:{\"name\":String(option.label)}})],1)]}},{key:\"selected-option\",fn:function(selectedOption){return [_c('span',{staticClass:\"option__icon\",class:selectedOption.icon}),_vm._v(\" \"),_c('span',{staticClass:\"option__title\"},[_c('NcEllipsisedOption',{attrs:{\"name\":String(selectedOption.label)}})],1)]}}])}),_vm._v(\" \"),(!_vm.isPredefined)?_c('input',{attrs:{\"type\":\"text\"},domProps:{\"value\":_vm.currentValue.pattern},on:{\"input\":_vm.updateCustom}}):_vm._e()],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestTime.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestTime.vue?vue&type=script&lang=js&\"","\n import API from \"!../../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestTime.vue?vue&type=style&index=0&id=6efe8c39&prod&scoped=true&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestTime.vue?vue&type=style&index=0&id=6efe8c39&prod&scoped=true&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./RequestTime.vue?vue&type=template&id=6efe8c39&scoped=true&\"\nimport script from \"./RequestTime.vue?vue&type=script&lang=js&\"\nexport * from \"./RequestTime.vue?vue&type=script&lang=js&\"\nimport style0 from \"./RequestTime.vue?vue&type=style&index=0&id=6efe8c39&prod&scoped=true&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"6efe8c39\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',{staticClass:\"timeslot\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.newValue.startTime),expression:\"newValue.startTime\"}],staticClass:\"timeslot--start\",attrs:{\"type\":\"text\",\"placeholder\":\"e.g. 08:00\"},domProps:{\"value\":(_vm.newValue.startTime)},on:{\"input\":[function($event){if($event.target.composing)return;_vm.$set(_vm.newValue, \"startTime\", $event.target.value)},_vm.update]}}),_vm._v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.newValue.endTime),expression:\"newValue.endTime\"}],attrs:{\"type\":\"text\",\"placeholder\":\"e.g. 18:00\"},domProps:{\"value\":(_vm.newValue.endTime)},on:{\"input\":[function($event){if($event.target.composing)return;_vm.$set(_vm.newValue, \"endTime\", $event.target.value)},_vm.update]}}),_vm._v(\" \"),(!_vm.valid)?_c('p',{staticClass:\"invalid-hint\"},[_vm._v(\"\\n\\t\\t\"+_vm._s(_vm.t('workflowengine', 'Please enter a valid time span'))+\"\\n\\t\")]):_vm._e(),_vm._v(\" \"),_c('NcSelect',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.valid),expression:\"valid\"}],attrs:{\"clearable\":false,\"options\":_vm.timezones},on:{\"input\":_vm.update},model:{value:(_vm.newValue.timezone),callback:function ($$v) {_vm.$set(_vm.newValue, \"timezone\", $$v)},expression:\"newValue.timezone\"}})],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n","import mod from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestURL.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestURL.vue?vue&type=script&lang=js&\"","\n import API from \"!../../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestURL.vue?vue&type=style&index=0&id=71932524&prod&scoped=true&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/sass-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestURL.vue?vue&type=style&index=0&id=71932524&prod&scoped=true&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./RequestURL.vue?vue&type=template&id=71932524&scoped=true&\"\nimport script from \"./RequestURL.vue?vue&type=script&lang=js&\"\nexport * from \"./RequestURL.vue?vue&type=script&lang=js&\"\nimport style0 from \"./RequestURL.vue?vue&type=style&index=0&id=71932524&prod&scoped=true&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"71932524\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',[_c('NcSelect',{attrs:{\"value\":_vm.currentValue,\"placeholder\":_vm.t('workflowengine', 'Select a request URL'),\"label\":\"label\",\"clearable\":false,\"options\":_vm.options},on:{\"input\":_vm.setValue},scopedSlots:_vm._u([{key:\"option\",fn:function(option){return [_c('span',{staticClass:\"option__icon\",class:option.icon}),_vm._v(\" \"),_c('span',{staticClass:\"option__title\"},[_c('NcEllipsisedOption',{attrs:{\"name\":String(option.label)}})],1)]}},{key:\"selected-option\",fn:function(selectedOption){return [_c('span',{staticClass:\"option__icon\",class:selectedOption.icon}),_vm._v(\" \"),_c('span',{staticClass:\"option__title\"},[_c('NcEllipsisedOption',{attrs:{\"name\":String(selectedOption.label)}})],1)]}}])}),_vm._v(\" \"),(!_vm.isPredefined)?_c('input',{attrs:{\"type\":\"text\",\"placeholder\":_vm.placeholder},domProps:{\"value\":_vm.currentValue.id},on:{\"input\":_vm.updateCustom}}):_vm._e()],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n","import mod from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestUserGroup.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../../node_modules/babel-loader/lib/index.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestUserGroup.vue?vue&type=script&lang=js&\"","\n import API from \"!../../../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../../../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../../../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../../../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../../../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../../../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestUserGroup.vue?vue&type=style&index=0&id=07a6a476&prod&scoped=true&lang=css&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../../../../node_modules/css-loader/dist/cjs.js!../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./RequestUserGroup.vue?vue&type=style&index=0&id=07a6a476&prod&scoped=true&lang=css&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./RequestUserGroup.vue?vue&type=template&id=07a6a476&scoped=true&\"\nimport script from \"./RequestUserGroup.vue?vue&type=script&lang=js&\"\nexport * from \"./RequestUserGroup.vue?vue&type=script&lang=js&\"\nimport style0 from \"./RequestUserGroup.vue?vue&type=style&index=0&id=07a6a476&prod&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"07a6a476\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',[_c('NcSelect',{attrs:{\"value\":_vm.currentValue,\"loading\":_vm.status.isLoading && _vm.groups.length === 0,\"options\":_vm.groups,\"clearable\":false,\"label\":\"displayname\",\"track-by\":\"id\"},on:{\"search-change\":_vm.searchAsync,\"input\":(value) => _vm.$emit('input', value.id)}})],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","/**\n * @copyright Copyright (c) 2019 Julius Härtl \n *\n * @author Julius Härtl \n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\nimport RequestUserAgent from './RequestUserAgent'\nimport RequestTime from './RequestTime'\nimport RequestURL from './RequestURL'\nimport RequestUserGroup from './RequestUserGroup'\n\nconst RequestChecks = [\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\RequestURL',\n\t\tname: t('workflowengine', 'Request URL'),\n\t\toperators: [\n\t\t\t{ operator: 'is', name: t('workflowengine', 'is') },\n\t\t\t{ operator: '!is', name: t('workflowengine', 'is not') },\n\t\t\t{ operator: 'matches', name: t('workflowengine', 'matches') },\n\t\t\t{ operator: '!matches', name: t('workflowengine', 'does not match') },\n\t\t],\n\t\tcomponent: RequestURL,\n\t},\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\RequestTime',\n\t\tname: t('workflowengine', 'Request time'),\n\t\toperators: [\n\t\t\t{ operator: 'in', name: t('workflowengine', 'between') },\n\t\t\t{ operator: '!in', name: t('workflowengine', 'not between') },\n\t\t],\n\t\tcomponent: RequestTime,\n\t},\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\RequestUserAgent',\n\t\tname: t('workflowengine', 'Request user agent'),\n\t\toperators: [\n\t\t\t{ operator: 'is', name: t('workflowengine', 'is') },\n\t\t\t{ operator: '!is', name: t('workflowengine', 'is not') },\n\t\t\t{ operator: 'matches', name: t('workflowengine', 'matches') },\n\t\t\t{ operator: '!matches', name: t('workflowengine', 'does not match') },\n\t\t],\n\t\tcomponent: RequestUserAgent,\n\t},\n\t{\n\t\tclass: 'OCA\\\\WorkflowEngine\\\\Check\\\\UserGroupMembership',\n\t\tname: t('workflowengine', 'User group membership'),\n\t\toperators: [\n\t\t\t{ operator: 'is', name: t('workflowengine', 'is member of') },\n\t\t\t{ operator: '!is', name: t('workflowengine', 'is not member of') },\n\t\t],\n\t\tcomponent: RequestUserGroup,\n\t},\n]\n\nexport default RequestChecks\n","/**\n * @copyright Copyright (c) 2019 Julius Härtl \n *\n * @author Julius Härtl \n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\nimport FileChecks from './file'\nimport RequestChecks from './request'\n\nexport default [...FileChecks, ...RequestChecks]\n","/**\n * @copyright Copyright (c) 2019 Julius Härtl \n *\n * @author John Molakvoæ \n * @author Julius Härtl \n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\nimport Vue from 'vue'\nimport Vuex from 'vuex'\nimport store from './store'\nimport Settings from './components/Workflow'\nimport ShippedChecks from './components/Checks'\n\n/**\n * A plugin for displaying a custom value field for checks\n *\n * @typedef {object} CheckPlugin\n * @property {string} class - The PHP class name of the check\n * @property {Comparison[]} operators - A list of possible comparison operations running on the check\n * @property {Vue} component - A vue component to handle the rendering of options\n * The component should handle the v-model directive properly,\n * so it needs a value property to receive data and emit an input\n * event once the data has changed\n * @property {Function} placeholder - Return a placeholder of no custom component is used\n * @property {Function} validate - validate a check if no custom component is used\n */\n\n/**\n * A plugin for extending the admin page representation of an operator\n *\n * @typedef {object} OperatorPlugin\n * @property {string} id - The PHP class name of the check\n * @property {string} operation - Default value for the operation field\n * @property {string} color - Custom color code to be applied for the operator selector\n * @property {Vue} component - A vue component to handle the rendering of options\n * The component should handle the v-model directive properly,\n * so it needs a value property to receive data and emit an input\n * event once the data has changed\n */\n\n/**\n * @typedef {object} Comparison\n * @property {string} operator - value the comparison should have, e.g. !less, greater\n * @property {string} name - Translated readable text, e.g. less or equals\n */\n\n/**\n * Public javascript api for apps to register custom plugins\n */\nwindow.OCA.WorkflowEngine = Object.assign({}, OCA.WorkflowEngine, {\n\n\t/**\n\t *\n\t * @param {CheckPlugin} Plugin the plugin to register\n\t */\n\tregisterCheck(Plugin) {\n\t\tstore.commit('addPluginCheck', Plugin)\n\t},\n\t/**\n\t *\n\t * @param {OperatorPlugin} Plugin the plugin to register\n\t */\n\tregisterOperator(Plugin) {\n\t\tstore.commit('addPluginOperator', Plugin)\n\t},\n})\n\n// Register shipped checks\nShippedChecks.forEach((checkPlugin) => window.OCA.WorkflowEngine.registerCheck(checkPlugin))\n\nVue.use(Vuex)\nVue.prototype.t = t\n\nconst View = Vue.extend(Settings)\nconst workflowengine = new View({\n\tstore,\n})\nworkflowengine.$mount('#workflowengine')\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".check[data-v-75b7ace8]{display:flex;flex-wrap:wrap;align-items:flex-start;width:100%;padding-right:20px}.check>*[data-v-75b7ace8]:not(.close){width:180px}.check>.comparator[data-v-75b7ace8]{min-width:200px;width:200px}.check>.option[data-v-75b7ace8]{min-width:260px;width:260px;min-height:48px}.check>.option>input[type=text][data-v-75b7ace8]{min-height:48px}.check>.v-select[data-v-75b7ace8],.check>.button-vue[data-v-75b7ace8],.check>input[type=text][data-v-75b7ace8]{margin-right:5px;margin-bottom:5px}input[type=text][data-v-75b7ace8]{margin:0}.invalid[data-v-75b7ace8]{border-color:var(--color-error) !important}\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Check.vue\"],\"names\":[],\"mappings\":\"AACA,wBACC,YAAA,CACA,cAAA,CACA,sBAAA,CACA,UAAA,CACA,kBAAA,CAEA,sCACC,WAAA,CAED,oCACC,eAAA,CACA,WAAA,CAED,gCACC,eAAA,CACA,WAAA,CACA,eAAA,CAEA,iDACC,eAAA,CAGF,+GAGC,gBAAA,CACA,iBAAA,CAGF,kCACC,QAAA,CAED,0BACC,0CAAA\",\"sourcesContent\":[\"\\n.check {\\n\\tdisplay: flex;\\n\\tflex-wrap: wrap;\\n\\talign-items: flex-start; // to not stretch components vertically\\n\\twidth: 100%;\\n\\tpadding-right: 20px;\\n\\n\\t& > *:not(.close) {\\n\\t\\twidth: 180px;\\n\\t}\\n\\t& > .comparator {\\n\\t\\tmin-width: 200px;\\n\\t\\twidth: 200px;\\n\\t}\\n\\t& > .option {\\n\\t\\tmin-width: 260px;\\n\\t\\twidth: 260px;\\n\\t\\tmin-height: 48px;\\n\\n\\t\\t& > input[type=text] {\\n\\t\\t\\tmin-height: 48px;\\n\\t\\t}\\n\\t}\\n\\t& > .v-select,\\n\\t& > .button-vue,\\n\\t& > input[type=text] {\\n\\t\\tmargin-right: 5px;\\n\\t\\tmargin-bottom: 5px;\\n\\t}\\n}\\ninput[type=text] {\\n\\tmargin: 0;\\n}\\n.invalid {\\n\\tborder-color: var(--color-error) !important;\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".v-select[data-v-a5701332],input[type=text][data-v-a5701332]{width:100%}input[type=text][data-v-a5701332]{min-height:48px}.option__icon[data-v-a5701332],.option__icon-img[data-v-a5701332]{display:inline-block;min-width:30px;background-position:center;vertical-align:middle}.option__icon-img[data-v-a5701332]{text-align:center}.option__title[data-v-a5701332]{display:inline-flex;width:calc(100% - 36px);vertical-align:middle}\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Checks/FileMimeType.vue\"],\"names\":[],\"mappings\":\"AACA,6DAEC,UAAA,CAGD,kCACC,eAAA,CAGD,kEAEC,oBAAA,CACA,cAAA,CACA,0BAAA,CACA,qBAAA,CAGD,mCACC,iBAAA,CAGD,gCACC,mBAAA,CACA,uBAAA,CACA,qBAAA\",\"sourcesContent\":[\"\\n.v-select,\\ninput[type='text'] {\\n\\twidth: 100%;\\n}\\n\\ninput[type=text] {\\n\\tmin-height: 48px;\\n}\\n\\n.option__icon,\\n.option__icon-img {\\n\\tdisplay: inline-block;\\n\\tmin-width: 30px;\\n\\tbackground-position: center;\\n\\tvertical-align: middle;\\n}\\n\\n.option__icon-img {\\n\\ttext-align: center;\\n}\\n\\n.option__title {\\n\\tdisplay: inline-flex;\\n\\twidth: calc(100% - 36px);\\n\\tvertical-align: middle;\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".timeslot[data-v-6efe8c39]{display:flex;flex-grow:1;flex-wrap:wrap;max-width:180px}.timeslot .multiselect[data-v-6efe8c39]{width:100%;margin-bottom:5px}.timeslot .multiselect[data-v-6efe8c39] .multiselect__tags:not(:hover):not(:focus):not(:active){border:1px solid rgba(0,0,0,0)}.timeslot input[type=text][data-v-6efe8c39]{width:50%;margin:0;margin-bottom:5px;min-height:48px}.timeslot input[type=text].timeslot--start[data-v-6efe8c39]{margin-right:5px;width:calc(50% - 5px)}.timeslot .invalid-hint[data-v-6efe8c39]{color:var(--color-text-maxcontrast)}\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Checks/RequestTime.vue\"],\"names\":[],\"mappings\":\"AACA,2BACC,YAAA,CACA,WAAA,CACA,cAAA,CACA,eAAA,CAEA,wCACC,UAAA,CACA,iBAAA,CAGD,gGACC,8BAAA,CAGD,4CACC,SAAA,CACA,QAAA,CACA,iBAAA,CACA,eAAA,CAEA,4DACC,gBAAA,CACA,qBAAA,CAIF,yCACC,mCAAA\",\"sourcesContent\":[\"\\n.timeslot {\\n\\tdisplay: flex;\\n\\tflex-grow: 1;\\n\\tflex-wrap: wrap;\\n\\tmax-width: 180px;\\n\\n\\t.multiselect {\\n\\t\\twidth: 100%;\\n\\t\\tmargin-bottom: 5px;\\n\\t}\\n\\n\\t.multiselect::v-deep .multiselect__tags:not(:hover):not(:focus):not(:active) {\\n\\t\\tborder: 1px solid transparent;\\n\\t}\\n\\n\\tinput[type=text] {\\n\\t\\twidth: 50%;\\n\\t\\tmargin: 0;\\n\\t\\tmargin-bottom: 5px;\\n\\t\\tmin-height: 48px;\\n\\n\\t\\t&.timeslot--start {\\n\\t\\t\\tmargin-right: 5px;\\n\\t\\t\\twidth: calc(50% - 5px);\\n\\t\\t}\\n\\t}\\n\\n\\t.invalid-hint {\\n\\t\\tcolor: var(--color-text-maxcontrast);\\n\\t}\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".v-select[data-v-71932524],input[type=text][data-v-71932524]{width:100%}input[type=text][data-v-71932524]{min-height:48px}.option__icon[data-v-71932524]{display:inline-block;min-width:30px;background-position:center;vertical-align:middle}.option__title[data-v-71932524]{display:inline-flex;width:calc(100% - 36px);vertical-align:middle}\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Checks/RequestURL.vue\"],\"names\":[],\"mappings\":\"AACA,6DAEC,UAAA,CAED,kCACC,eAAA,CAGD,+BACC,oBAAA,CACA,cAAA,CACA,0BAAA,CACA,qBAAA,CAGD,gCACC,mBAAA,CACA,uBAAA,CACA,qBAAA\",\"sourcesContent\":[\"\\n.v-select,\\ninput[type='text'] {\\n\\twidth: 100%;\\n}\\ninput[type='text'] {\\n\\tmin-height: 48px;\\n}\\n\\n.option__icon {\\n\\tdisplay: inline-block;\\n\\tmin-width: 30px;\\n\\tbackground-position: center;\\n\\tvertical-align: middle;\\n}\\n\\n.option__title {\\n\\tdisplay: inline-flex;\\n\\twidth: calc(100% - 36px);\\n\\tvertical-align: middle;\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".event[data-v-01681aaa]{margin-bottom:5px}.isComplex img[data-v-01681aaa]{vertical-align:text-top}.isComplex span[data-v-01681aaa]{padding-top:2px;display:inline-block}.multiselect[data-v-01681aaa]{width:100%;max-width:550px;margin-top:4px}.multiselect[data-v-01681aaa] .multiselect__single{display:flex}.multiselect[data-v-01681aaa]:not(.multiselect--active) .multiselect__tags{background-color:var(--color-main-background) !important;border:1px solid rgba(0,0,0,0)}.multiselect[data-v-01681aaa] .multiselect__tags{background-color:var(--color-main-background) !important;height:auto;min-height:34px}.multiselect[data-v-01681aaa]:not(.multiselect--disabled) .multiselect__tags .multiselect__single{background-image:var(--icon-triangle-s-dark);background-repeat:no-repeat;background-position:right center}input[data-v-01681aaa]{border:1px solid rgba(0,0,0,0)}.option__title[data-v-01681aaa]{margin-left:5px;color:var(--color-main-text)}.option__title_single[data-v-01681aaa]{font-weight:900}.option__icon[data-v-01681aaa]{width:16px;height:16px;filter:var(--background-invert-if-dark)}.eventlist img[data-v-01681aaa],.eventlist .text[data-v-01681aaa]{vertical-align:middle}\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Event.vue\"],\"names\":[],\"mappings\":\"AACA,wBACC,iBAAA,CAGA,gCACC,uBAAA,CAED,iCACC,eAAA,CACA,oBAAA,CAGF,8BACC,UAAA,CACA,eAAA,CACA,cAAA,CAED,mDACC,YAAA,CAED,2EACC,wDAAA,CACA,8BAAA,CAGD,iDACC,wDAAA,CACA,WAAA,CACA,eAAA,CAGD,kGACC,4CAAA,CACA,2BAAA,CACA,gCAAA,CAGD,uBACC,8BAAA,CAGD,gCACC,eAAA,CACA,4BAAA,CAED,uCACC,eAAA,CAGD,+BACC,UAAA,CACA,WAAA,CACA,uCAAA,CAGD,kEAEC,qBAAA\",\"sourcesContent\":[\"\\n.event {\\n\\tmargin-bottom: 5px;\\n}\\n.isComplex {\\n\\timg {\\n\\t\\tvertical-align: text-top;\\n\\t}\\n\\tspan {\\n\\t\\tpadding-top: 2px;\\n\\t\\tdisplay: inline-block;\\n\\t}\\n}\\n.multiselect {\\n\\twidth: 100%;\\n\\tmax-width: 550px;\\n\\tmargin-top: 4px;\\n}\\n.multiselect::v-deep .multiselect__single {\\n\\tdisplay: flex;\\n}\\n.multiselect:not(.multiselect--active)::v-deep .multiselect__tags {\\n\\tbackground-color: var(--color-main-background) !important;\\n\\tborder: 1px solid transparent;\\n}\\n\\n.multiselect::v-deep .multiselect__tags {\\n\\tbackground-color: var(--color-main-background) !important;\\n\\theight: auto;\\n\\tmin-height: 34px;\\n}\\n\\n.multiselect:not(.multiselect--disabled)::v-deep .multiselect__tags .multiselect__single {\\n\\tbackground-image: var(--icon-triangle-s-dark);\\n\\tbackground-repeat: no-repeat;\\n\\tbackground-position: right center;\\n}\\n\\ninput {\\n\\tborder: 1px solid transparent;\\n}\\n\\n.option__title {\\n\\tmargin-left: 5px;\\n\\tcolor: var(--color-main-text);\\n}\\n.option__title_single {\\n\\tfont-weight: 900;\\n}\\n\\n.option__icon {\\n\\twidth: 16px;\\n\\theight: 16px;\\n\\tfilter: var(--background-invert-if-dark);\\n}\\n\\n.eventlist img,\\n.eventlist .text {\\n\\tvertical-align: middle;\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".actions__item[data-v-90eae5ec]{display:flex;flex-wrap:wrap;flex-direction:column;flex-grow:1;margin-left:-1px;padding:10px;border-radius:var(--border-radius-large);margin-right:20px;margin-bottom:20px}.actions__item .icon[data-v-90eae5ec]{display:block;width:100%;height:50px;background-size:50px 50px;background-position:center center;margin-top:10px;margin-bottom:10px;background-repeat:no-repeat}.actions__item__description[data-v-90eae5ec]{text-align:center;flex-grow:1;display:flex;flex-direction:column;align-items:center}.actions__item_options[data-v-90eae5ec]{width:100%;margin-top:10px;padding-left:60px}h3[data-v-90eae5ec],small[data-v-90eae5ec]{padding:6px;display:block}h3[data-v-90eae5ec]{margin:0;padding:0;font-weight:600}small[data-v-90eae5ec]{font-size:10pt;flex-grow:1}.colored[data-v-90eae5ec]:not(.more){background-color:var(--color-primary-element)}.colored:not(.more) h3[data-v-90eae5ec],.colored:not(.more) small[data-v-90eae5ec]{color:var(--color-primary-text)}.actions__item[data-v-90eae5ec]:not(.colored){flex-direction:row}.actions__item:not(.colored) .actions__item__description[data-v-90eae5ec]{padding-top:5px;text-align:left;width:calc(100% - 105px)}.actions__item:not(.colored) .actions__item__description small[data-v-90eae5ec]{padding:0}.actions__item:not(.colored) .icon[data-v-90eae5ec]{width:50px;margin:0;margin-right:10px}.actions__item:not(.colored) .icon[data-v-90eae5ec]:not(.icon-invert){filter:var(--background-invert-if-bright)}.colored .icon-invert[data-v-90eae5ec]{filter:var(--background-invert-if-bright)}\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/styles/operation.scss\"],\"names\":[],\"mappings\":\"AAAA,gCACC,YAAA,CACA,cAAA,CACA,qBAAA,CACA,WAAA,CACA,gBAAA,CACA,YAAA,CACA,wCAAA,CACA,iBAAA,CACA,kBAAA,CAED,sCACC,aAAA,CACA,UAAA,CACA,WAAA,CACA,yBAAA,CACA,iCAAA,CACA,eAAA,CACA,kBAAA,CACA,2BAAA,CAED,6CACC,iBAAA,CACA,WAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CAED,wCACC,UAAA,CACA,eAAA,CACA,iBAAA,CAED,2CACC,WAAA,CACA,aAAA,CAED,oBACC,QAAA,CACA,SAAA,CACA,eAAA,CAED,uBACC,cAAA,CACA,WAAA,CAGD,qCACC,6CAAA,CACA,mFACC,+BAAA,CAIF,8CACC,kBAAA,CAEA,0EACC,eAAA,CACA,eAAA,CACA,wBAAA,CACA,gFACC,SAAA,CAGF,oDACC,UAAA,CACA,QAAA,CACA,iBAAA,CACA,sEACC,yCAAA,CAKH,uCACC,yCAAA\",\"sourcesContent\":[\".actions__item {\\n\\tdisplay: flex;\\n\\tflex-wrap: wrap;\\n\\tflex-direction: column;\\n\\tflex-grow: 1;\\n\\tmargin-left: -1px;\\n\\tpadding: 10px;\\n\\tborder-radius: var(--border-radius-large);\\n\\tmargin-right: 20px;\\n\\tmargin-bottom: 20px;\\n}\\n.actions__item .icon {\\n\\tdisplay: block;\\n\\twidth: 100%;\\n\\theight: 50px;\\n\\tbackground-size: 50px 50px;\\n\\tbackground-position: center center;\\n\\tmargin-top: 10px;\\n\\tmargin-bottom: 10px;\\n\\tbackground-repeat: no-repeat;\\n}\\n.actions__item__description {\\n\\ttext-align: center;\\n\\tflex-grow: 1;\\n\\tdisplay: flex;\\n\\tflex-direction: column;\\n\\talign-items: center;\\n}\\n.actions__item_options {\\n\\twidth: 100%;\\n\\tmargin-top: 10px;\\n\\tpadding-left: 60px;\\n}\\nh3, small {\\n\\tpadding: 6px;\\n\\tdisplay: block;\\n}\\nh3 {\\n\\tmargin: 0;\\n\\tpadding: 0;\\n\\tfont-weight: 600;\\n}\\nsmall {\\n\\tfont-size: 10pt;\\n\\tflex-grow: 1;\\n}\\n\\n.colored:not(.more) {\\n\\tbackground-color: var(--color-primary-element);\\n\\th3, small {\\n\\t\\tcolor: var(--color-primary-text)\\n\\t}\\n}\\n\\n.actions__item:not(.colored) {\\n\\tflex-direction: row;\\n\\n\\t.actions__item__description {\\n\\t\\tpadding-top: 5px;\\n\\t\\ttext-align: left;\\n\\t\\twidth: calc(100% - 105px);\\n\\t\\tsmall {\\n\\t\\t\\tpadding: 0;\\n\\t\\t}\\n\\t}\\n\\t.icon {\\n\\t\\twidth: 50px;\\n\\t\\tmargin: 0;\\n\\t\\tmargin-right: 10px;\\n\\t\\t&:not(.icon-invert) {\\n\\t\\t\\tfilter: var(--background-invert-if-bright);\\n\\t\\t}\\n\\t}\\n}\\n\\n.colored .icon-invert {\\n\\tfilter: var(--background-invert-if-bright);\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".buttons[data-v-4bee7716]{display:flex;justify-content:end}.buttons button[data-v-4bee7716]{margin-left:5px}.buttons button[data-v-4bee7716]:last-child{margin-right:10px}.error-message[data-v-4bee7716]{float:right;margin-right:10px}.flow-icon[data-v-4bee7716]{width:44px}.rule[data-v-4bee7716]{display:flex;flex-wrap:wrap;border-left:5px solid var(--color-primary-element)}.rule .trigger[data-v-4bee7716],.rule .action[data-v-4bee7716]{flex-grow:1;min-height:100px;max-width:920px}.rule .action[data-v-4bee7716]{max-width:400px;position:relative}.rule .icon-confirm[data-v-4bee7716]{background-position:right 27px;padding-right:20px;margin-right:20px}.trigger p[data-v-4bee7716],.action p[data-v-4bee7716]{min-height:34px;display:flex}.trigger p>span[data-v-4bee7716],.action p>span[data-v-4bee7716]{min-width:50px;text-align:right;color:var(--color-text-maxcontrast);padding-right:10px;padding-top:6px}.trigger p .multiselect[data-v-4bee7716],.action p .multiselect[data-v-4bee7716]{flex-grow:1;max-width:300px}.trigger p:first-child span[data-v-4bee7716]{padding-top:3px}.trigger p[data-v-4bee7716]:last-child{padding-top:8px}.check--add[data-v-4bee7716]{background-position:7px center;background-color:rgba(0,0,0,0);padding-left:6px;margin:0;width:180px;border-radius:var(--border-radius);color:var(--color-text-maxcontrast);font-weight:normal;text-align:left;font-size:1em}@media(max-width: 1400px){.rule[data-v-4bee7716],.rule .trigger[data-v-4bee7716],.rule .action[data-v-4bee7716]{width:100%;max-width:100%}.rule .flow-icon[data-v-4bee7716]{display:none}}\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Rule.vue\"],\"names\":[],\"mappings\":\"AAEA,0BACC,YAAA,CACA,mBAAA,CAEA,iCACC,eAAA,CAED,4CACC,iBAAA,CAIF,gCACC,WAAA,CACA,iBAAA,CAGD,4BACC,UAAA,CAGD,uBACC,YAAA,CACA,cAAA,CACA,kDAAA,CAEA,+DAEC,WAAA,CACA,gBAAA,CACA,eAAA,CAED,+BACC,eAAA,CACA,iBAAA,CAED,qCACC,8BAAA,CACA,kBAAA,CACA,iBAAA,CAGF,uDACC,eAAA,CACA,YAAA,CAEA,iEACC,cAAA,CACA,gBAAA,CACA,mCAAA,CACA,kBAAA,CACA,eAAA,CAED,iFACC,WAAA,CACA,eAAA,CAGF,6CACE,eAAA,CAEF,uCACE,eAAA,CAGF,6BACC,8BAAA,CACA,8BAAA,CACA,gBAAA,CACA,QAAA,CACA,WAAA,CACA,kCAAA,CACA,mCAAA,CACA,kBAAA,CACA,eAAA,CACA,aAAA,CAGD,0BAEE,sFACC,UAAA,CACA,cAAA,CAED,kCACC,YAAA,CAAA\",\"sourcesContent\":[\"\\n\\n.buttons {\\n\\tdisplay: flex;\\n\\tjustify-content: end;\\n\\n\\tbutton {\\n\\t\\tmargin-left: 5px;\\n\\t}\\n\\tbutton:last-child{\\n\\t\\tmargin-right: 10px;\\n\\t}\\n}\\n\\n.error-message {\\n\\tfloat: right;\\n\\tmargin-right: 10px;\\n}\\n\\n.flow-icon {\\n\\twidth: 44px;\\n}\\n\\n.rule {\\n\\tdisplay: flex;\\n\\tflex-wrap: wrap;\\n\\tborder-left: 5px solid var(--color-primary-element);\\n\\n\\t.trigger,\\n\\t.action {\\n\\t\\tflex-grow: 1;\\n\\t\\tmin-height: 100px;\\n\\t\\tmax-width: 920px;\\n\\t}\\n\\t.action {\\n\\t\\tmax-width: 400px;\\n\\t\\tposition: relative;\\n\\t}\\n\\t.icon-confirm {\\n\\t\\tbackground-position: right 27px;\\n\\t\\tpadding-right: 20px;\\n\\t\\tmargin-right: 20px;\\n\\t}\\n}\\n.trigger p, .action p {\\n\\tmin-height: 34px;\\n\\tdisplay: flex;\\n\\n\\t& > span {\\n\\t\\tmin-width: 50px;\\n\\t\\ttext-align: right;\\n\\t\\tcolor: var(--color-text-maxcontrast);\\n\\t\\tpadding-right: 10px;\\n\\t\\tpadding-top: 6px;\\n\\t}\\n\\t.multiselect {\\n\\t\\tflex-grow: 1;\\n\\t\\tmax-width: 300px;\\n\\t}\\n}\\n.trigger p:first-child span {\\n\\t\\tpadding-top: 3px;\\n}\\n.trigger p:last-child {\\n\\t\\tpadding-top: 8px;\\n}\\n\\n.check--add {\\n\\tbackground-position: 7px center;\\n\\tbackground-color: transparent;\\n\\tpadding-left: 6px;\\n\\tmargin: 0;\\n\\twidth: 180px;\\n\\tborder-radius: var(--border-radius);\\n\\tcolor: var(--color-text-maxcontrast);\\n\\tfont-weight: normal;\\n\\ttext-align: left;\\n\\tfont-size: 1em;\\n}\\n\\n@media (max-width:1400px) {\\n\\t.rule {\\n\\t\\t&, .trigger, .action {\\n\\t\\t\\twidth: 100%;\\n\\t\\t\\tmax-width: 100%;\\n\\t\\t}\\n\\t\\t.flow-icon {\\n\\t\\t\\tdisplay: none;\\n\\t\\t}\\n\\t}\\n}\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"#workflowengine[data-v-38a7a2e5]{border-bottom:1px solid var(--color-border)}.section[data-v-38a7a2e5]{max-width:100vw}.section h2.configured-flows[data-v-38a7a2e5]{margin-top:50px;margin-bottom:0}.actions[data-v-38a7a2e5]{display:flex;flex-wrap:wrap;max-width:1200px}.actions .actions__item[data-v-38a7a2e5]{max-width:280px;flex-basis:250px}.actions__more[data-v-38a7a2e5]{margin-bottom:10px}.slide-enter-active[data-v-38a7a2e5]{-moz-transition-duration:.3s;-webkit-transition-duration:.3s;-o-transition-duration:.3s;transition-duration:.3s;-moz-transition-timing-function:ease-in;-webkit-transition-timing-function:ease-in;-o-transition-timing-function:ease-in;transition-timing-function:ease-in}.slide-leave-active[data-v-38a7a2e5]{-moz-transition-duration:.3s;-webkit-transition-duration:.3s;-o-transition-duration:.3s;transition-duration:.3s;-moz-transition-timing-function:cubic-bezier(0, 1, 0.5, 1);-webkit-transition-timing-function:cubic-bezier(0, 1, 0.5, 1);-o-transition-timing-function:cubic-bezier(0, 1, 0.5, 1);transition-timing-function:cubic-bezier(0, 1, 0.5, 1)}.slide-enter-to[data-v-38a7a2e5],.slide-leave[data-v-38a7a2e5]{max-height:500px;overflow:hidden}.slide-enter[data-v-38a7a2e5],.slide-leave-to[data-v-38a7a2e5]{overflow:hidden;max-height:0;padding-top:0;padding-bottom:0}.actions__item[data-v-38a7a2e5]{display:flex;flex-wrap:wrap;flex-direction:column;flex-grow:1;margin-left:-1px;padding:10px;border-radius:var(--border-radius-large);margin-right:20px;margin-bottom:20px}.actions__item .icon[data-v-38a7a2e5]{display:block;width:100%;height:50px;background-size:50px 50px;background-position:center center;margin-top:10px;margin-bottom:10px;background-repeat:no-repeat}.actions__item__description[data-v-38a7a2e5]{text-align:center;flex-grow:1;display:flex;flex-direction:column;align-items:center}.actions__item_options[data-v-38a7a2e5]{width:100%;margin-top:10px;padding-left:60px}h3[data-v-38a7a2e5],small[data-v-38a7a2e5]{padding:6px;display:block}h3[data-v-38a7a2e5]{margin:0;padding:0;font-weight:600}small[data-v-38a7a2e5]{font-size:10pt;flex-grow:1}.colored[data-v-38a7a2e5]:not(.more){background-color:var(--color-primary-element)}.colored:not(.more) h3[data-v-38a7a2e5],.colored:not(.more) small[data-v-38a7a2e5]{color:var(--color-primary-text)}.actions__item[data-v-38a7a2e5]:not(.colored){flex-direction:row}.actions__item:not(.colored) .actions__item__description[data-v-38a7a2e5]{padding-top:5px;text-align:left;width:calc(100% - 105px)}.actions__item:not(.colored) .actions__item__description small[data-v-38a7a2e5]{padding:0}.actions__item:not(.colored) .icon[data-v-38a7a2e5]{width:50px;margin:0;margin-right:10px}.actions__item:not(.colored) .icon[data-v-38a7a2e5]:not(.icon-invert){filter:var(--background-invert-if-bright)}.colored .icon-invert[data-v-38a7a2e5]{filter:var(--background-invert-if-bright)}.actions__item.more[data-v-38a7a2e5]{background-color:var(--color-background-dark)}\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Workflow.vue\",\"webpack://./apps/workflowengine/src/styles/operation.scss\"],\"names\":[],\"mappings\":\"AACA,iCACC,2CAAA,CAED,0BACC,eAAA,CAEA,8CACC,eAAA,CACA,eAAA,CAGF,0BACC,YAAA,CACA,cAAA,CACA,gBAAA,CACA,yCACC,eAAA,CACA,gBAAA,CAGF,gCACC,kBAAA,CAGD,qCACC,4BAAA,CACA,+BAAA,CACA,0BAAA,CACA,uBAAA,CACA,uCAAA,CACA,0CAAA,CACA,qCAAA,CACA,kCAAA,CAGD,qCACC,4BAAA,CACA,+BAAA,CACA,0BAAA,CACA,uBAAA,CACA,0DAAA,CACA,6DAAA,CACA,wDAAA,CACA,qDAAA,CAGD,+DACC,gBAAA,CACA,eAAA,CAGD,+DACC,eAAA,CACA,YAAA,CACA,aAAA,CACA,gBAAA,CCxDD,gCACC,YAAA,CACA,cAAA,CACA,qBAAA,CACA,WAAA,CACA,gBAAA,CACA,YAAA,CACA,wCAAA,CACA,iBAAA,CACA,kBAAA,CAED,sCACC,aAAA,CACA,UAAA,CACA,WAAA,CACA,yBAAA,CACA,iCAAA,CACA,eAAA,CACA,kBAAA,CACA,2BAAA,CAED,6CACC,iBAAA,CACA,WAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CAED,wCACC,UAAA,CACA,eAAA,CACA,iBAAA,CAED,2CACC,WAAA,CACA,aAAA,CAED,oBACC,QAAA,CACA,SAAA,CACA,eAAA,CAED,uBACC,cAAA,CACA,WAAA,CAGD,qCACC,6CAAA,CACA,mFACC,+BAAA,CAIF,8CACC,kBAAA,CAEA,0EACC,eAAA,CACA,eAAA,CACA,wBAAA,CACA,gFACC,SAAA,CAGF,oDACC,UAAA,CACA,QAAA,CACA,iBAAA,CACA,sEACC,yCAAA,CAKH,uCACC,yCAAA,CDfD,qCACC,6CAAA\",\"sourcesContent\":[\"\\n#workflowengine {\\n\\tborder-bottom: 1px solid var(--color-border);\\n}\\n.section {\\n\\tmax-width: 100vw;\\n\\n\\th2.configured-flows {\\n\\t\\tmargin-top: 50px;\\n\\t\\tmargin-bottom: 0;\\n\\t}\\n}\\n.actions {\\n\\tdisplay: flex;\\n\\tflex-wrap: wrap;\\n\\tmax-width: 1200px;\\n\\t.actions__item {\\n\\t\\tmax-width: 280px;\\n\\t\\tflex-basis: 250px;\\n\\t}\\n}\\n.actions__more {\\n\\tmargin-bottom: 10px;\\n}\\n\\n.slide-enter-active {\\n\\t-moz-transition-duration: 0.3s;\\n\\t-webkit-transition-duration: 0.3s;\\n\\t-o-transition-duration: 0.3s;\\n\\ttransition-duration: 0.3s;\\n\\t-moz-transition-timing-function: ease-in;\\n\\t-webkit-transition-timing-function: ease-in;\\n\\t-o-transition-timing-function: ease-in;\\n\\ttransition-timing-function: ease-in;\\n}\\n\\n.slide-leave-active {\\n\\t-moz-transition-duration: 0.3s;\\n\\t-webkit-transition-duration: 0.3s;\\n\\t-o-transition-duration: 0.3s;\\n\\ttransition-duration: 0.3s;\\n\\t-moz-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\\n\\t-webkit-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\\n\\t-o-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\\n\\ttransition-timing-function: cubic-bezier(0, 1, 0.5, 1);\\n}\\n\\n.slide-enter-to, .slide-leave {\\n\\tmax-height: 500px;\\n\\toverflow: hidden;\\n}\\n\\n.slide-enter, .slide-leave-to {\\n\\toverflow: hidden;\\n\\tmax-height: 0;\\n\\tpadding-top: 0;\\n\\tpadding-bottom: 0;\\n}\\n\\n@import \\\"./../styles/operation\\\";\\n\\n.actions__item.more {\\n\\tbackground-color: var(--color-background-dark);\\n}\\n\",\".actions__item {\\n\\tdisplay: flex;\\n\\tflex-wrap: wrap;\\n\\tflex-direction: column;\\n\\tflex-grow: 1;\\n\\tmargin-left: -1px;\\n\\tpadding: 10px;\\n\\tborder-radius: var(--border-radius-large);\\n\\tmargin-right: 20px;\\n\\tmargin-bottom: 20px;\\n}\\n.actions__item .icon {\\n\\tdisplay: block;\\n\\twidth: 100%;\\n\\theight: 50px;\\n\\tbackground-size: 50px 50px;\\n\\tbackground-position: center center;\\n\\tmargin-top: 10px;\\n\\tmargin-bottom: 10px;\\n\\tbackground-repeat: no-repeat;\\n}\\n.actions__item__description {\\n\\ttext-align: center;\\n\\tflex-grow: 1;\\n\\tdisplay: flex;\\n\\tflex-direction: column;\\n\\talign-items: center;\\n}\\n.actions__item_options {\\n\\twidth: 100%;\\n\\tmargin-top: 10px;\\n\\tpadding-left: 60px;\\n}\\nh3, small {\\n\\tpadding: 6px;\\n\\tdisplay: block;\\n}\\nh3 {\\n\\tmargin: 0;\\n\\tpadding: 0;\\n\\tfont-weight: 600;\\n}\\nsmall {\\n\\tfont-size: 10pt;\\n\\tflex-grow: 1;\\n}\\n\\n.colored:not(.more) {\\n\\tbackground-color: var(--color-primary-element);\\n\\th3, small {\\n\\t\\tcolor: var(--color-primary-text)\\n\\t}\\n}\\n\\n.actions__item:not(.colored) {\\n\\tflex-direction: row;\\n\\n\\t.actions__item__description {\\n\\t\\tpadding-top: 5px;\\n\\t\\ttext-align: left;\\n\\t\\twidth: calc(100% - 105px);\\n\\t\\tsmall {\\n\\t\\t\\tpadding: 0;\\n\\t\\t}\\n\\t}\\n\\t.icon {\\n\\t\\twidth: 50px;\\n\\t\\tmargin: 0;\\n\\t\\tmargin-right: 10px;\\n\\t\\t&:not(.icon-invert) {\\n\\t\\t\\tfilter: var(--background-invert-if-bright);\\n\\t\\t}\\n\\t}\\n}\\n\\n.colored .icon-invert {\\n\\tfilter: var(--background-invert-if-bright);\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.v-select[data-v-6769046e],\\ninput[type='text'][data-v-6769046e] {\\n\\twidth: 100%;\\n}\\ninput[type='text'][data-v-6769046e] {\\n\\tmin-height: 48px;\\n}\\n.option__icon[data-v-6769046e] {\\n\\tdisplay: inline-block;\\n\\tmin-width: 30px;\\n\\tbackground-position: center;\\n\\tvertical-align: middle;\\n}\\n.option__title[data-v-6769046e] {\\n\\tdisplay: inline-flex;\\n\\twidth: calc(100% - 36px);\\n\\tvertical-align: middle;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Checks/RequestUserAgent.vue\"],\"names\":[],\"mappings\":\";AAmJA;;CAEA,WAAA;AACA;AACA;CACA,gBAAA;AACA;AAEA;CACA,qBAAA;CACA,eAAA;CACA,2BAAA;CACA,sBAAA;AACA;AAEA;CACA,oBAAA;CACA,wBAAA;CACA,sBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.v-select[data-v-07a6a476] {\\n\\twidth: 100%;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./apps/workflowengine/src/components/Checks/RequestUserGroup.vue\"],\"names\":[],\"mappings\":\";AA4GA;CACA,WAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","var map = {\n\t\"./af\": 42786,\n\t\"./af.js\": 42786,\n\t\"./ar\": 30867,\n\t\"./ar-dz\": 14130,\n\t\"./ar-dz.js\": 14130,\n\t\"./ar-kw\": 96135,\n\t\"./ar-kw.js\": 96135,\n\t\"./ar-ly\": 56440,\n\t\"./ar-ly.js\": 56440,\n\t\"./ar-ma\": 47702,\n\t\"./ar-ma.js\": 47702,\n\t\"./ar-sa\": 16040,\n\t\"./ar-sa.js\": 16040,\n\t\"./ar-tn\": 37100,\n\t\"./ar-tn.js\": 37100,\n\t\"./ar.js\": 30867,\n\t\"./az\": 31083,\n\t\"./az.js\": 31083,\n\t\"./be\": 9808,\n\t\"./be.js\": 9808,\n\t\"./bg\": 68338,\n\t\"./bg.js\": 68338,\n\t\"./bm\": 67438,\n\t\"./bm.js\": 67438,\n\t\"./bn\": 8905,\n\t\"./bn-bd\": 76225,\n\t\"./bn-bd.js\": 76225,\n\t\"./bn.js\": 8905,\n\t\"./bo\": 11560,\n\t\"./bo.js\": 11560,\n\t\"./br\": 1278,\n\t\"./br.js\": 1278,\n\t\"./bs\": 80622,\n\t\"./bs.js\": 80622,\n\t\"./ca\": 2468,\n\t\"./ca.js\": 2468,\n\t\"./cs\": 5822,\n\t\"./cs.js\": 5822,\n\t\"./cv\": 50877,\n\t\"./cv.js\": 50877,\n\t\"./cy\": 47373,\n\t\"./cy.js\": 47373,\n\t\"./da\": 24780,\n\t\"./da.js\": 24780,\n\t\"./de\": 59740,\n\t\"./de-at\": 60217,\n\t\"./de-at.js\": 60217,\n\t\"./de-ch\": 60894,\n\t\"./de-ch.js\": 60894,\n\t\"./de.js\": 59740,\n\t\"./dv\": 5300,\n\t\"./dv.js\": 5300,\n\t\"./el\": 50837,\n\t\"./el.js\": 50837,\n\t\"./en-au\": 78348,\n\t\"./en-au.js\": 78348,\n\t\"./en-ca\": 77925,\n\t\"./en-ca.js\": 77925,\n\t\"./en-gb\": 22243,\n\t\"./en-gb.js\": 22243,\n\t\"./en-ie\": 46436,\n\t\"./en-ie.js\": 46436,\n\t\"./en-il\": 47207,\n\t\"./en-il.js\": 47207,\n\t\"./en-in\": 44175,\n\t\"./en-in.js\": 44175,\n\t\"./en-nz\": 76319,\n\t\"./en-nz.js\": 76319,\n\t\"./en-sg\": 31662,\n\t\"./en-sg.js\": 31662,\n\t\"./eo\": 92915,\n\t\"./eo.js\": 92915,\n\t\"./es\": 55655,\n\t\"./es-do\": 55251,\n\t\"./es-do.js\": 55251,\n\t\"./es-mx\": 96112,\n\t\"./es-mx.js\": 96112,\n\t\"./es-us\": 71146,\n\t\"./es-us.js\": 71146,\n\t\"./es.js\": 55655,\n\t\"./et\": 5603,\n\t\"./et.js\": 5603,\n\t\"./eu\": 77763,\n\t\"./eu.js\": 77763,\n\t\"./fa\": 76959,\n\t\"./fa.js\": 76959,\n\t\"./fi\": 11897,\n\t\"./fi.js\": 11897,\n\t\"./fil\": 42549,\n\t\"./fil.js\": 42549,\n\t\"./fo\": 94694,\n\t\"./fo.js\": 94694,\n\t\"./fr\": 94470,\n\t\"./fr-ca\": 63049,\n\t\"./fr-ca.js\": 63049,\n\t\"./fr-ch\": 52330,\n\t\"./fr-ch.js\": 52330,\n\t\"./fr.js\": 94470,\n\t\"./fy\": 5044,\n\t\"./fy.js\": 5044,\n\t\"./ga\": 29295,\n\t\"./ga.js\": 29295,\n\t\"./gd\": 2101,\n\t\"./gd.js\": 2101,\n\t\"./gl\": 38794,\n\t\"./gl.js\": 38794,\n\t\"./gom-deva\": 27884,\n\t\"./gom-deva.js\": 27884,\n\t\"./gom-latn\": 23168,\n\t\"./gom-latn.js\": 23168,\n\t\"./gu\": 95349,\n\t\"./gu.js\": 95349,\n\t\"./he\": 24206,\n\t\"./he.js\": 24206,\n\t\"./hi\": 30094,\n\t\"./hi.js\": 30094,\n\t\"./hr\": 30316,\n\t\"./hr.js\": 30316,\n\t\"./hu\": 22138,\n\t\"./hu.js\": 22138,\n\t\"./hy-am\": 11423,\n\t\"./hy-am.js\": 11423,\n\t\"./id\": 29218,\n\t\"./id.js\": 29218,\n\t\"./is\": 90135,\n\t\"./is.js\": 90135,\n\t\"./it\": 90626,\n\t\"./it-ch\": 10150,\n\t\"./it-ch.js\": 10150,\n\t\"./it.js\": 90626,\n\t\"./ja\": 39183,\n\t\"./ja.js\": 39183,\n\t\"./jv\": 24286,\n\t\"./jv.js\": 24286,\n\t\"./ka\": 12105,\n\t\"./ka.js\": 12105,\n\t\"./kk\": 47772,\n\t\"./kk.js\": 47772,\n\t\"./km\": 18758,\n\t\"./km.js\": 18758,\n\t\"./kn\": 79282,\n\t\"./kn.js\": 79282,\n\t\"./ko\": 33730,\n\t\"./ko.js\": 33730,\n\t\"./ku\": 1408,\n\t\"./ku.js\": 1408,\n\t\"./ky\": 33291,\n\t\"./ky.js\": 33291,\n\t\"./lb\": 36841,\n\t\"./lb.js\": 36841,\n\t\"./lo\": 55466,\n\t\"./lo.js\": 55466,\n\t\"./lt\": 57010,\n\t\"./lt.js\": 57010,\n\t\"./lv\": 37595,\n\t\"./lv.js\": 37595,\n\t\"./me\": 39861,\n\t\"./me.js\": 39861,\n\t\"./mi\": 35493,\n\t\"./mi.js\": 35493,\n\t\"./mk\": 95966,\n\t\"./mk.js\": 95966,\n\t\"./ml\": 87341,\n\t\"./ml.js\": 87341,\n\t\"./mn\": 5115,\n\t\"./mn.js\": 5115,\n\t\"./mr\": 10370,\n\t\"./mr.js\": 10370,\n\t\"./ms\": 9847,\n\t\"./ms-my\": 41237,\n\t\"./ms-my.js\": 41237,\n\t\"./ms.js\": 9847,\n\t\"./mt\": 72126,\n\t\"./mt.js\": 72126,\n\t\"./my\": 56165,\n\t\"./my.js\": 56165,\n\t\"./nb\": 64924,\n\t\"./nb.js\": 64924,\n\t\"./ne\": 16744,\n\t\"./ne.js\": 16744,\n\t\"./nl\": 93901,\n\t\"./nl-be\": 59814,\n\t\"./nl-be.js\": 59814,\n\t\"./nl.js\": 93901,\n\t\"./nn\": 83877,\n\t\"./nn.js\": 83877,\n\t\"./oc-lnc\": 92135,\n\t\"./oc-lnc.js\": 92135,\n\t\"./pa-in\": 15858,\n\t\"./pa-in.js\": 15858,\n\t\"./pl\": 64495,\n\t\"./pl.js\": 64495,\n\t\"./pt\": 89520,\n\t\"./pt-br\": 57971,\n\t\"./pt-br.js\": 57971,\n\t\"./pt.js\": 89520,\n\t\"./ro\": 96459,\n\t\"./ro.js\": 96459,\n\t\"./ru\": 21793,\n\t\"./ru.js\": 21793,\n\t\"./sd\": 40950,\n\t\"./sd.js\": 40950,\n\t\"./se\": 10490,\n\t\"./se.js\": 10490,\n\t\"./si\": 90124,\n\t\"./si.js\": 90124,\n\t\"./sk\": 64249,\n\t\"./sk.js\": 64249,\n\t\"./sl\": 14985,\n\t\"./sl.js\": 14985,\n\t\"./sq\": 51104,\n\t\"./sq.js\": 51104,\n\t\"./sr\": 49131,\n\t\"./sr-cyrl\": 79915,\n\t\"./sr-cyrl.js\": 79915,\n\t\"./sr.js\": 49131,\n\t\"./ss\": 85893,\n\t\"./ss.js\": 85893,\n\t\"./sv\": 98760,\n\t\"./sv.js\": 98760,\n\t\"./sw\": 91172,\n\t\"./sw.js\": 91172,\n\t\"./ta\": 27333,\n\t\"./ta.js\": 27333,\n\t\"./te\": 23110,\n\t\"./te.js\": 23110,\n\t\"./tet\": 52095,\n\t\"./tet.js\": 52095,\n\t\"./tg\": 27321,\n\t\"./tg.js\": 27321,\n\t\"./th\": 9041,\n\t\"./th.js\": 9041,\n\t\"./tk\": 19005,\n\t\"./tk.js\": 19005,\n\t\"./tl-ph\": 75768,\n\t\"./tl-ph.js\": 75768,\n\t\"./tlh\": 89444,\n\t\"./tlh.js\": 89444,\n\t\"./tr\": 72397,\n\t\"./tr.js\": 72397,\n\t\"./tzl\": 28254,\n\t\"./tzl.js\": 28254,\n\t\"./tzm\": 51106,\n\t\"./tzm-latn\": 30699,\n\t\"./tzm-latn.js\": 30699,\n\t\"./tzm.js\": 51106,\n\t\"./ug-cn\": 9288,\n\t\"./ug-cn.js\": 9288,\n\t\"./uk\": 67691,\n\t\"./uk.js\": 67691,\n\t\"./ur\": 13795,\n\t\"./ur.js\": 13795,\n\t\"./uz\": 6791,\n\t\"./uz-latn\": 60588,\n\t\"./uz-latn.js\": 60588,\n\t\"./uz.js\": 6791,\n\t\"./vi\": 65666,\n\t\"./vi.js\": 65666,\n\t\"./x-pseudo\": 14378,\n\t\"./x-pseudo.js\": 14378,\n\t\"./yo\": 75805,\n\t\"./yo.js\": 75805,\n\t\"./zh-cn\": 83839,\n\t\"./zh-cn.js\": 83839,\n\t\"./zh-hk\": 55726,\n\t\"./zh-hk.js\": 55726,\n\t\"./zh-mo\": 99807,\n\t\"./zh-mo.js\": 99807,\n\t\"./zh-tw\": 74152,\n\t\"./zh-tw.js\": 74152\n};\n\n\nfunction webpackContext(req) {\n\tvar id = webpackContextResolve(req);\n\treturn __webpack_require__(id);\n}\nfunction webpackContextResolve(req) {\n\tif(!__webpack_require__.o(map, req)) {\n\t\tvar e = new Error(\"Cannot find module '\" + req + \"'\");\n\t\te.code = 'MODULE_NOT_FOUND';\n\t\tthrow e;\n\t}\n\treturn map[req];\n}\nwebpackContext.keys = function webpackContextKeys() {\n\treturn Object.keys(map);\n};\nwebpackContext.resolve = webpackContextResolve;\nmodule.exports = webpackContext;\nwebpackContext.id = 46700;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\tid: moduleId,\n\t\tloaded: false,\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Flag the module as loaded\n\tmodule.loaded = true;\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = function(module) {\n\tvar getter = module && module.__esModule ?\n\t\tfunction() { return module['default']; } :\n\t\tfunction() { return module; };\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }","// define __esModule on exports\n__webpack_require__.r = function(exports) {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","__webpack_require__.nmd = function(module) {\n\tmodule.paths = [];\n\tif (!module.children) module.children = [];\n\treturn module;\n};","__webpack_require__.j = 8318;","__webpack_require__.b = document.baseURI || self.location.href;\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t8318: 0\n};\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = function(parentChunkLoadingFunction, data) {\n\tvar chunkIds = data[0];\n\tvar moreModules = data[1];\n\tvar runtime = data[2];\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunknextcloud\"] = self[\"webpackChunknextcloud\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","__webpack_require__.nc = undefined;","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [7874], function() { return __webpack_require__(37682); })\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n"],"names":["deferred","scopeValue","loadState","getApiUrl","url","generateOcsUrl","Vue","Vuex","Store","state","rules","scope","appstoreEnabled","operations","plugins","checks","operators","entities","events","map","entity","event","id","eventName","flat","mutations","addRule","rule","push","valid","updateRule","index","findIndex","item","newRule","Object","assign","removeRule","splice","addPluginCheck","plugin","class","addPluginOperator","color","actions","fetchRules","context","axios","data","values","ocs","forEach","commit","createNewRule","isComplex","fixedEntity","find","Date","getTime","name","operator","value","operation","JSON","parse","pushUpdateRule","confirmPassword","result","deleteRule","setValid","getters","getRules","filter","sort","rule1","rule2","getOperationForRule","getEntityForOperation","getEventsForOperation","getChecksForEntity","check","supportedEntities","indexOf","length","reduce","obj","components","NcMultiselect","props","type","required","computed","allEvents","currentEvent","methods","updateEvent","newEntity","showWarning","options","styleTagTransform","setAttributes","insert","domAPI","insertStyleElement","_vm","this","_c","_self","staticClass","attrs","icon","_v","_s","triggerHint","on","scopedSlots","_u","key","fn","isOpen","_l","displayName","_e","option","NcActionButton","NcActions","NcSelect","CloseIcon","directives","ClickOutside","deleteVisible","currentOption","currentOperator","currentComponent","valuePlaceholder","watch","mounted","showDelete","hideDelete","validate","updateCheck","rawName","expression","ref","t","model","callback","$$v","component","tag","$event","$set","invalid","domProps","target","composing","$emit","proxy","NcButton","colored","default","style","backgroundColor","iconClass","backgroundImage","description","_t","ArrowRight","Check","CheckMark","Close","Event","Operation","Tooltip","editing","error","dirty","originalRule","ruleStatus","title","tooltip","placement","show","content","lastCheckComplete","updateOperation","saveRule","console","cancelRule","removeCheck","onAddFilter","borderLeftColor","MenuDown","MenuUp","Rule","NcSettingsSection","showMoreOperations","appstoreUrl","workflowDocUrl","mapGetters","mapState","hasMoreOperations","getMainOperations","showAppStoreHint","nativeOn","regexRegex","regexIPv4","regexIPv6","String","newValue","immediate","handler","updateInternalValue","NcEllipsisedOption","mixins","valueMixin","predefinedTypes","label","iconUrl","isPredefined","customValue","currentValue","validateRegex","setValue","updateCustom","selectedOption","NcSelectTags","beforeMount","updateValue","update","stringOrRegexOperators","placeholder","string","exec","FileMimeType","match","validateIPv4","FileSystemTag","MfaVerifiedValue","matchingPredefined","pattern","timezones","startTime","endTime","timezone","moment","isLoading","groups","status","searchAsync","searchQuery","response","displayname","addGroup","RequestURL","RequestTime","RequestUserAgent","RequestUserGroup","FileChecks","RequestChecks","window","OCA","WorkflowEngine","registerCheck","Plugin","store","registerOperator","ShippedChecks","checkPlugin","Settings","$mount","___CSS_LOADER_EXPORT___","module","webpackContext","req","webpackContextResolve","__webpack_require__","o","e","Error","code","keys","resolve","exports","__webpack_module_cache__","moduleId","cachedModule","undefined","loaded","__webpack_modules__","call","m","O","chunkIds","priority","notFulfilled","Infinity","i","fulfilled","j","every","r","n","getter","__esModule","d","a","definition","defineProperty","enumerable","get","g","globalThis","Function","prop","prototype","hasOwnProperty","Symbol","toStringTag","nmd","paths","children","b","document","baseURI","self","location","href","installedChunks","chunkId","webpackJsonpCallback","parentChunkLoadingFunction","moreModules","runtime","some","chunkLoadingGlobal","bind","nc","__webpack_exports__"],"sourceRoot":""} \ No newline at end of file From be63a27c4dce1b8192fb27ccdc853e9824c14c38 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 17 Dec 2024 08:16:01 +0000 Subject: [PATCH 026/184] refactor: ocm test runner into multiple functions --- dev/ocm-test-suite.sh | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/dev/ocm-test-suite.sh b/dev/ocm-test-suite.sh index b4dd3d37..e2dda948 100755 --- a/dev/ocm-test-suite.sh +++ b/dev/ocm-test-suite.sh @@ -2,7 +2,7 @@ # ----------------------------------------------------------------------------------- # Script to Automate EFSS OCM Test Suite Execution -# Author: Mohammad Mahdi Baghbani Pourvahid +# Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # Description: @@ -25,7 +25,7 @@ # EFSS_PLATFORM_2 : (Optional) Secondary EFSS platform for interop tests (default: "nextcloud"). # EFSS_PLATFORM_2_VERSION : (Optional) Version of the secondary EFSS platform (default: "v27.1.11"). -# TODO @MahdiBaghbani: How about more documentation? what do we exatly need to run these tests? +# TODO @MahdiBaghbani: How about more documentation? what do we exatly need to run these tests? # Requirements: # - Test scripts must exist in the folder structure: dev/ocm-test-suite//.sh # - Required tools and dependencies must be installed. @@ -236,27 +236,27 @@ main() { # Validate test case. case "${test_case}" in - "login"|"share-with"|"share-link"|"invite-link") - ;; - *) - error_exit "Unknown test case: '${test_case}'. Valid options are: login, share-with, share-link, invite-link." - ;; + "login" | "share-with" | "share-link" | "invite-link") ;; + + *) + error_exit "Unknown test case: '${test_case}'. Valid options are: login, share-with, share-link, invite-link." + ;; esac # Route the test case to the appropriate handler. case "$test_case" in - "login") - handle_login "${efss_platform_1}" "${efss_platform_1_version}" "${script_mode}" "${browser_platform}" - ;; - "share-with") - handle_share_with "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" - ;; - "share-link") - handle_share_link "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" - ;; - "invite-link") - handle_invite_link "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" - ;; + "login") + handle_login "${efss_platform_1}" "${efss_platform_1_version}" "${script_mode}" "${browser_platform}" + ;; + "share-with") + handle_share_with "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" + ;; + "share-link") + handle_share_link "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" + ;; + "invite-link") + handle_invite_link "${efss_platform_1}" "${efss_platform_1_version}" "${efss_platform_2}" "${efss_platform_2_version}" "${script_mode}" "${browser_platform}" + ;; esac } From 4d6ac39d376f8c02442530c89d36c779fb78db46 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Thu, 19 Dec 2024 11:15:54 +0000 Subject: [PATCH 027/184] [no ci] refactor: better way to build and tag our efss versions --- docker/build/all.sh | 300 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 245 insertions(+), 55 deletions(-) diff --git a/docker/build/all.sh b/docker/build/all.sh index 2705b048..adc4c464 100755 --- a/docker/build/all.sh +++ b/docker/build/all.sh @@ -1,82 +1,272 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e +# ----------------------------------------------------------------------------------- +# Docker Build Script for PonderSource Development Images +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) +# Description: +# This script automates the building of various Docker images used in the PonderSource +# development environment. It supports enabling Docker BuildKit and incorporates a +# cache-busting mechanism for pulling fresh source code during builds. +# +# Requirements: +# - Docker must be installed and accessible. +# - The script should be placed in a directory that has a 'dockerfiles' subdirectory, +# which contains the respective Dockerfiles for each image. +# +# Usage: +# ./all.sh [USE_BUILDKIT] +# +# Arguments: +# USE_BUILDKIT (optional): Set to 0 or 1 to disable or enable BuildKit. +# Defaults to 1 (enabled). +# +# Notes: +# - The script changes the working directory to the parent directory of the script's +# location. Ensure that 'dockerfiles' is accessible from there. +# - CACHEBUST is used to force Docker to re-pull or rebuild layers as needed. +# - Each build is attempted, and if any image fails to build, the script moves on +# to the next image after printing an error message. +# +# Example: +# ./all.sh # Enable BuildKit (default) +# ./all.sh 0 # Disable BuildKit +# ----------------------------------------------------------------------------------- -cd "${DIR}/.." || exit +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -eo pipefail -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} -export DOCKER_BUILDKIT="${USE_BUILDKIT}" +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "$script_dir/.." || error_exit "Failed to change directory to script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} -echo Building pondersource/dev-stock-ocmstub -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/ocmstub.Dockerfile --tag pondersource/dev-stock-ocmstub:v1.0.0 --tag pondersource/dev-stock-ocmstub:latest . +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "$message" >&2 +} -echo Building pondersource/dev-stock-revad -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/revad.Dockerfile --tag pondersource/dev-stock-revad:latest . +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} -echo Building pondersource/dev-stock-php-base -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v30.0.0" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.0 --tag pondersource/dev-stock-nextcloud:latest . +# ----------------------------------------------------------------------------------- +# Docker Build Function +# ----------------------------------------------------------------------------------- +# A helper function to streamline the Docker build process. +# Arguments: +# 1. Dockerfile path (relative to the current working directory) +# 2. Image name +# 3. Tags (space-separated string of tags) +# 4. Cache Bust to force rebuild. +# 5. Additional build arguments (optional) +# +# The function: +# - Validates the Dockerfile existence. +# - Prints a build message and runs 'docker build' with specified args. +# - Applies a CACHEBUST build-arg by default to help with cache invalidation. +# - Prints success or error messages accordingly. +build_docker_image() { + local dockerfile="${1}" + local image_name="${2}" + local tags="${3}" + local cache_bust="${4}" + local build_args="${5}" -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v29.0.8" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.8 . + # Validate that the Dockerfile exists + if [[ ! -f "./dockerfiles/${dockerfile}" ]]; then + printf "Error: Dockerfile not found at '%s'. Skipping build of %s.\n" "${dockerfile}" "${image_name}" >&2 + return 1 + fi -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v28.0.12" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.12 . + printf "Building image: %s from Dockerfile: %s\n" "${image_name}" "${dockerfile}" + if ! docker build \ + --build-arg CACHEBUST="${cache_bust}" ${build_args} \ + --file "./dockerfiles/${dockerfile}" \ + $(for tag in ${tags}; do printf -- "--tag ${image_name}:%s " "${tag}"; done) \ + .; then + printf "Error: Failed to build image %s.\n" "${image_name}" >&2 + return 1 + fi -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg BRANCH_NEXTCLOUD="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . + printf "Successfully built: %s\n\n" "${image_name}" +} -# echo Building pondersource/dev-stock-nextcloud-sunet -# docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-sunet.Dockerfile --tag pondersource/dev-stock-nextcloud-sunet . +# ----------------------------------------------------------------------------------- +# Function: main +# Purpose: Main function to manage the flow of the script. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment. + initialize_environment -# cd simple-saml-php + # ----------------------------------------------------------------------------------- + # Enable Docker BuildKit (Optional) + # ----------------------------------------------------------------------------------- + # Allow enabling or disabling BuildKit via the first script argument. + # Default: BuildKit enabled (value 1). + USE_BUILDKIT=${1:-1} + export DOCKER_BUILDKIT="${USE_BUILDKIT}" -# echo Building pondersource/dev-stock-simple-saml-php -# docker build --tag pondersource/dev-stock-simple-saml-php . + # export BUILDKIT_PROGRESS=plain -# cd .. + # ----------------------------------------------------------------------------------- + # Build Images + # ----------------------------------------------------------------------------------- + # Below is a list of images to build along with their Dockerfiles and tags. + # Modify these as necessary to fit your environment and requirements. -echo Building pondersource/dev-stock-nextcloud-solid -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-solid.Dockerfile --tag pondersource/dev-stock-nextcloud-solid:latest . + # OCM Stub + build_docker_image ocmstub.Dockerfile pondersource/ocmstub "v1.0.0 latest" DEFAULT -echo Building pondersource/dev-stock-nextcloud-sciencemesh -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-sciencemesh.Dockerfile --tag pondersource/dev-stock-nextcloud-sciencemesh:latest . + # Revad + build_docker_image revad.Dockerfile pondersource/revad "latest" DEFAULT -echo Building pondersource/dev-stock-owncloud -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud.Dockerfile --tag pondersource/dev-stock-owncloud:latest . + # PHP Base + # build_docker_image php-base.Dockerfile pondersource/php-base "latest" DEFAULT -echo Building pondersource/dev-stock-owncloud-sciencemesh -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-sciencemesh.Dockerfile --tag pondersource/dev-stock-owncloud-sciencemesh:latest . + # Nextcloud Base + build_docker_image nextcloud-base.Dockerfile pondersource/nextcloud-base "latest" DEFAULT -echo Building pondersource/dev-stock-owncloud-surf-trashbin -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-surf-trashbin.Dockerfile --tag pondersource/dev-stock-owncloud-surf-trashbin:latest . + # Nextcloud Versions + # The first element in this array is considered the "latest". + nextcloud_versions=("v30.0.2" "v29.0.10" "v28.0.14" "v27.1.11") -echo Building pondersource/dev-stock-owncloud-token-based-access -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-token-based-access.Dockerfile --tag pondersource/dev-stock-owncloud-token-based-access:latest . + # shellcheck disable=SC2207 + # TODO @MahdiBaghbani: Decide that if we want to do this automatically or manually. + # Automatically get latest images + # nextcloud_versions=($(curl -s https://api.github.com/repos/nextcloud/server/releases?per_page=100 | \ + # jq -r '.[].tag_name' | \ + # grep -E '^v(2[7-9]|[3-9][0-9])\.[0-9]+\.[0-9]+$' | \ + # sort --version-sort -r | \ awk -F '.' '!seen[$1]++')) -echo Building pondersource/dev-stock-owncloud-opencloudmesh -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-opencloudmesh.Dockerfile --tag pondersource/dev-stock-owncloud-opencloudmesh:latest . + # Iterate over the array of versions + for i in "${!nextcloud_versions[@]}"; do + version="${nextcloud_versions[i]}" -echo Building pondersource/dev-stock-owncloud-federatedgroups -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-federatedgroups.Dockerfile --tag pondersource/dev-stock-owncloud-federatedgroups:latest . + # If this is the first element (index 0), also add the "latest" tag + if [[ "$i" -eq 0 ]]; then + tags="${version} latest" + else + tags="${version}" + fi -echo Building pondersource/dev-stock-owncloud-ocm-test-suite -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-ocm-test-suite.Dockerfile --tag pondersource/dev-stock-owncloud-ocm-test-suite:latest . + # Build the Docker image with the determined tags and build-arg + build_docker_image \ + nextcloud.Dockerfile \ + pondersource/nextcloud \ + "${tags}" \ + DEFAULT \ + "--build-arg NEXTCLOUD_BRANCH=${version}" + done + + # nextcloud Variants + # build_docker_image nextcloud-solid.Dockerfile pondersource/nextcloud-solid "latest" DEFAULT + # build_docker_image nextcloud-sciencemesh.Dockerfile pondersource/nextcloud-sciencemesh "latest" DEFAULT + + # ownCloud Base + build_docker_image owncloud-base.Dockerfile pondersource/owncloud-base "latest" DEFAULT + + # ownCloud Versions + # The first element in this array is considered the "latest". + owncloud_versions=("v10.15.0") + + # Iterate over the array of versions + for i in "${!owncloud_versions[@]}"; do + version="${owncloud_versions[i]}" + + # If this is the first element (index 0), also add the "latest" tag + if [[ "$i" -eq 0 ]]; then + tags="${version} latest" + else + tags="${version}" + fi + + # Build the Docker image with the determined tags and build-arg + build_docker_image \ + owncloud.Dockerfile \ + pondersource/owncloud \ + "${tags}" \ + DEFAULT \ + "--build-arg OWNCLOUD_BRANCH=${version}" + done + + # OwnCloud Variants + # build_docker_image owncloud-sciencemesh.Dockerfile pondersource/owncloud-sciencemesh "latest" DEFAULT + # build_docker_image owncloud-surf-trashbin.Dockerfile pondersource/owncloud-surf-trashbin "latest" DEFAULT + # build_docker_image owncloud-token-based-access.Dockerfile pondersource/owncloud-token-based-access "latest" DEFAULT + # build_docker_image owncloud-opencloudmesh.Dockerfile pondersource/owncloud-opencloudmesh "latest" DEFAULT + # build_docker_image owncloud-federatedgroups.Dockerfile pondersource/owncloud-federatedgroups "latest" DEFAULT + # build_docker_image owncloud-ocm-test-suite.Dockerfile pondersource/owncloud-ocm-test-suite "latest" DEFAULT + + # ----------------------------------------------------------------------------------- + # Completion Message + # ----------------------------------------------------------------------------------- + printf "All builds attempted.\n" + printf "Check the above output for any build failures or errors.\n" +} + +# ----------------------------------------------------------------------------------- +# Execute the main function and pass all script arguments. +# ----------------------------------------------------------------------------------- +main "$@" From 400e0fb74eb2a25bb682ecb43c0693a0ca369163 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 12:58:08 +0000 Subject: [PATCH 028/184] update: dockerfiles to pass nextcloud tests --- docker/dockerfiles/nextcloud-base.Dockerfile | 6 +- docker/dockerfiles/nextcloud.Dockerfile | 5 +- docker/dockerfiles/ocmstub.Dockerfile | 108 +++++++++-- docker/dockerfiles/owncloud-base.Dockerfile | 167 ++++++++++++++++ docker/dockerfiles/owncloud.Dockerfile | 71 +++---- docker/dockerfiles/revad.Dockerfile | 191 +++++++++++++------ 6 files changed, 424 insertions(+), 124 deletions(-) create mode 100644 docker/dockerfiles/owncloud-base.Dockerfile diff --git a/docker/dockerfiles/nextcloud-base.Dockerfile b/docker/dockerfiles/nextcloud-base.Dockerfile index 05650616..e5ba52e9 100644 --- a/docker/dockerfiles/nextcloud-base.Dockerfile +++ b/docker/dockerfiles/nextcloud-base.Dockerfile @@ -11,7 +11,7 @@ LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" RUN set -ex; \ \ apt-get update; \ - apt-get install -y --no-install-recommends \ + apt-get install --no-install-recommends --assume-yes \ git \ vim \ curl\ @@ -23,6 +23,7 @@ RUN set -ex; \ ca-certificates \ libmagickcore-6.q16-6-extra \ ; \ + apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ \ mkdir -p /var/spool/cron/crontabs; \ @@ -37,7 +38,7 @@ RUN set -ex; \ savedAptMark="$(apt-mark showmanual)"; \ \ apt-get update; \ - apt-get install -y --no-install-recommends \ + apt-get install --no-install-recommends --assume-yes \ libcurl4-openssl-dev \ libevent-dev \ libfreetype6-dev \ @@ -101,6 +102,7 @@ RUN set -ex; \ | xargs -rt apt-mark manual; \ \ apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + apt-get clean; \ rm -rf /var/lib/apt/lists/* # set recommended PHP.ini settings diff --git a/docker/dockerfiles/nextcloud.Dockerfile b/docker/dockerfiles/nextcloud.Dockerfile index 816b15d1..70c66711 100644 --- a/docker/dockerfiles/nextcloud.Dockerfile +++ b/docker/dockerfiles/nextcloud.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-nextcloud-base:latest +FROM pondersource/nextcloud-base:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys @@ -28,6 +28,9 @@ RUN set -ex; \ mkdir -p /usr/src/nextcloud/custom_apps; \ chmod +x /usr/src/nextcloud/occ +# After cloning, `git` is no longer needed at runtime, so remove it to reduce image size. +RUN apt-get purge -y git && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* + COPY ./scripts/nextcloud/*.sh / COPY ./scripts/nextcloud/upgrade.exclude / COPY ./configs/nextcloud/* /usr/src/nextcloud/config/ diff --git a/docker/dockerfiles/ocmstub.Dockerfile b/docker/dockerfiles/ocmstub.Dockerfile index f320f4c9..a5dcc203 100644 --- a/docker/dockerfiles/ocmstub.Dockerfile +++ b/docker/dockerfiles/ocmstub.Dockerfile @@ -1,33 +1,99 @@ -FROM node +# ---------------------------------------------------------------------------- +# Base Image +# ---------------------------------------------------------------------------- +# Start from an official Node.js image based on Debian. +# Using a specific Node.js version for stability and reproducibility. +FROM node:23.4.0-bookworm@sha256:0b50ca11d81b5ed2622ff8770f040cdd4bd93a2561208c01c0c5db98bd65d551 -# keys for oci taken from: -# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys -LABEL org.opencontainers.image.licenses=MIT +# ---------------------------------------------------------------------------- +# OCI Image Metadata +# ---------------------------------------------------------------------------- +# Provide metadata that describes this image, its source, and authorship. +LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.title="PonderSource OCM Stub Image" LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" -RUN apt update -RUN apt install -yq iproute2 git +# ---------------------------------------------------------------------------- +# Install Required Packages +# ---------------------------------------------------------------------------- +# Update package list and install: +# - iproute2: for network utilities, may assist with debugging inside the container +# - git: required to clone the repository +# Use --no-install-recommends to avoid unnecessary packages and reduce image size. +RUN apt-get update && apt-get install --no-install-recommends --assume-yes \ + iproute2 \ + git; \ + apt-get clean && rm -rf /var/lib/apt/lists/* -ARG REPO_OCMSTUB=https://github.com/pondersource/ocm-stub -ARG BRANCH_OCMSTUB=mahdi/fix-grants -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . -# $RANDOM returns random number each time. +# ---------------------------------------------------------------------------- +# Build Arguments +# ---------------------------------------------------------------------------- +# These allow customizing which repository and branch to clone at build time. +# CACHEBUST is used to force rebuild steps when needed. +ARG OCMSTUB_REPO=https://github.com/pondersource/ocm-stub +ARG OCMSTUB_BRANCH=mahdi/fix-grants ARG CACHEBUST="default" -RUN git clone \ - --depth 1 \ - --recursive \ - --shallow-submodules \ - --branch ${BRANCH_OCMSTUB} \ - ${REPO_OCMSTUB} \ - /ocmstub +# ---------------------------------------------------------------------------- +# Clone Repository +# ---------------------------------------------------------------------------- +# Clone the specified branch of the OCM stub repository with minimal depth +# to reduce build time and image size. Also fetch submodules if present. +RUN git clone \ + --depth 1 \ + --recursive \ + --shallow-submodules \ + --branch ${OCMSTUB_BRANCH} \ + ${OCMSTUB_REPO} \ + /ocmstub; \ + rm -rf /ocmstub/.git + +# After cloning, `git` is no longer needed at runtime, so remove it to reduce image size. +RUN apt-get purge -y git && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* + +# ---------------------------------------------------------------------------- +# Set Working Directory +# ---------------------------------------------------------------------------- +# Set the working directory to the application directory. WORKDIR /ocmstub -RUN npm install +# ---------------------------------------------------------------------------- +# Install Dependencies +# ---------------------------------------------------------------------------- +# Use npm ci to install dependencies as listed in package-lock.json for reproducibility. +# --production ensures only production dependencies are installed, reducing size. +RUN npm ci --production -# run the app +# ---------------------------------------------------------------------------- +# Expose Ports +# ---------------------------------------------------------------------------- +# The application listens on HTTPS port 443. EXPOSE 443/tcp -CMD NODE_TLS_REJECT_UNAUTHORIZED=0 node stub.js \ No newline at end of file + +# ---------------------------------------------------------------------------- +# Runtime Configuration +# ---------------------------------------------------------------------------- +# NODE_TLS_REJECT_UNAUTHORIZED=0 allows connections even to self-signed TLS certificates. +# This is helpful for local testing but should not be used in production environments. +ENV NODE_TLS_REJECT_UNAUTHORIZED=0 + +# ---------------------------------------------------------------------------- +# Switch to Non-Root User +# ---------------------------------------------------------------------------- +# The base Node image provides a 'node' user. We'll run as 'node' for better security. +RUN chown -R node:node /ocmstub +USER node + +# ---------------------------------------------------------------------------- +# Healthcheck +# ---------------------------------------------------------------------------- +# Check if the application responds on port 443. Using curl with -k to ignore TLS. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -k -f https://localhost:443 || exit 1 + +# ---------------------------------------------------------------------------- +# Startup Command +# ---------------------------------------------------------------------------- +# Finally, run the Node.js application defined in stub.js. +CMD ["node", "stub.js"] diff --git a/docker/dockerfiles/owncloud-base.Dockerfile b/docker/dockerfiles/owncloud-base.Dockerfile new file mode 100644 index 00000000..10004180 --- /dev/null +++ b/docker/dockerfiles/owncloud-base.Dockerfile @@ -0,0 +1,167 @@ +FROM php:7.4.33-apache-bullseye@sha256:c9d7e608f73832673479770d66aacc8100011ec751d1905ff63fae3fe2e0ca6d + +# keys for oci taken from: +# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys +LABEL org.opencontainers.image.licenses=MIT +LABEL org.opencontainers.image.title="PonderSource ownCloud Base Image" +LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" +LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" + +# entrypoint.sh and cron.sh dependencies +RUN set -ex; \ + \ + apt-get update; \ + apt-get install --no-install-recommends --assume-yes \ + git \ + vim \ + curl\ + bzip2 \ + rsync \ + iproute2 \ + busybox-static \ + libldap-common \ + ca-certificates \ + libmagickcore-6.q16-6-extra \ + ; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* + +# install the PHP extensions we need +ENV PHP_MEMORY_LIMIT 512M +ENV PHP_UPLOAD_LIMIT 512M +RUN set -ex; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install --no-install-recommends --assume-yes \ + libcurl4-openssl-dev \ + libevent-dev \ + libfreetype6-dev \ + libgmp-dev \ + libicu-dev \ + libjpeg-dev \ + libldap2-dev \ + libmagickwand-dev \ + libmcrypt-dev \ + libmemcached-dev \ + libpng-dev \ + libpq-dev \ + libwebp-dev \ + libxml2-dev \ + libzip-dev \ + ; \ + \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \ + docker-php-ext-install -j "$(nproc)" \ + bcmath \ + exif \ + gd \ + gmp \ + intl \ + ldap \ + opcache \ + pcntl \ + pdo_mysql \ + pdo_pgsql \ + sysvsem \ + zip \ + ; \ + \ + # pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu-5.1.24; \ + pecl install imagick-3.7.0; \ + pecl install memcached-3.3.0; \ + pecl install redis-6.1.0; \ + \ + docker-php-ext-enable \ + apcu \ + imagick \ + memcached \ + redis \ + ; \ + rm -r /tmp/pear; \ + \ + # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \ + | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); print so }' \ + | sort -u \ + | xargs -r dpkg-query --search \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge --assume-yes --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* + +# set recommended PHP.ini settings +RUN { \ + echo 'opcache.enable=1'; \ + echo 'opcache.interned_strings_buffer=32'; \ + echo 'opcache.max_accelerated_files=10000'; \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.save_comments=1'; \ + echo 'opcache.revalidate_freq=60'; \ + echo 'opcache.jit=1255'; \ + echo 'opcache.jit_buffer_size=128M'; \ + } > "${PHP_INI_DIR}/conf.d/opcache-recommended.ini"; \ + \ + echo 'apc.enable_cli=1' >> "${PHP_INI_DIR}/conf.d/docker-php-ext-apcu.ini"; \ + \ + { \ + echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \ + echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \ + echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \ + } > "${PHP_INI_DIR}/conf.d/owncloud.ini"; \ + \ + mkdir /var/www/data; \ + mkdir -p /docker-entrypoint-hooks.d/pre-installation \ + /docker-entrypoint-hooks.d/post-installation \ + /docker-entrypoint-hooks.d/pre-upgrade \ + /docker-entrypoint-hooks.d/post-upgrade \ + /docker-entrypoint-hooks.d/before-starting; \ + chown -R www-data:root /var/www; \ + chmod -R g=u /var/www + +VOLUME /var/www/html + +COPY ./tls/certificates/* /tls/ +COPY ./tls/certificate-authority/* /tls/ +RUN ln --symbolic --force /tls/*.crt /usr/local/share/ca-certificates; \ + update-ca-certificates + +COPY ./configs/owncloud/apache.conf /etc/apache2/sites-enabled/000-default.conf + +RUN a2enmod headers rewrite remoteip ssl; \ + { \ + echo 'RemoteIPHeader X-Real-IP'; \ + echo 'RemoteIPInternalProxy 10.0.0.0/8'; \ + echo 'RemoteIPInternalProxy 172.16.0.0/12'; \ + echo 'RemoteIPInternalProxy 192.168.0.0/16'; \ + } > /etc/apache2/conf-available/remoteip.conf; \ + a2enconf remoteip; \ + chown -R www-data:root /var/log/apache2; \ + chmod -R g=u /var/log/apache2 + +# set apache config LimitRequestBody +ENV APACHE_BODY_LIMIT 1073741824 +RUN { \ + echo 'LimitRequestBody ${APACHE_BODY_LIMIT}'; \ + } > /etc/apache2/conf-available/apache-limits.conf; \ + a2enconf apache-limits + +RUN curl --silent --show-error https://getcomposer.org/installer -o /root/composer-setup.php +RUN php /root/composer-setup.php --install-dir=/usr/local/bin --filename=composer + +# install nodejs and yarn. +RUN curl --silent --location https://deb.nodesource.com/setup_18.x | bash -; \ + apt-get install --no-install-recommends --assume-yes nodejs; \ + npm install --global yarn \ + ; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* diff --git a/docker/dockerfiles/owncloud.Dockerfile b/docker/dockerfiles/owncloud.Dockerfile index e388914c..cfc14ffc 100644 --- a/docker/dockerfiles/owncloud.Dockerfile +++ b/docker/dockerfiles/owncloud.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-php-base:latest +FROM pondersource/owncloud-base:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys @@ -7,48 +7,37 @@ LABEL org.opencontainers.image.title="PonderSource ownCloud Image" LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" -RUN rm --recursive --force /var/www/html -USER www-data +ARG OWNCLOUD_REPO=https://github.com/owncloud/core +ARG OWNCLOUD_BRANCH=v10.15.0 -ARG REPO_OWNCLOUD=https://github.com/owncloud/core -ARG BRANCH_OWNCLOUD=v10.15.0 # CACHEBUST forces docker to clone fresh source codes from git. # example: docker build -t your-image --build-arg CACHEBUST="default" . # $RANDOM returns random number each time. ARG CACHEBUST="default" -RUN git clone \ - --depth 1 \ - --recursive \ - --shallow-submodules \ - --branch ${BRANCH_OWNCLOUD} \ - ${REPO_OWNCLOUD} \ - html - -USER root -WORKDIR /var/www/html - -# switch php version for ownCloud. -RUN switch-php.sh 7.4 - -ENV PHP_MEMORY_LIMIT="512M" - -RUN curl --silent --show-error https://getcomposer.org/installer -o /root/composer-setup.php -RUN php /root/composer-setup.php --install-dir=/usr/local/bin --filename=composer - -# install nodejs and yarn. -RUN curl --silent --location https://deb.nodesource.com/setup_18.x | bash - -RUN apt install nodejs -RUN npm install --global yarn - -USER www-data -# this file can be overrided in docker run or docker compose.yaml. -# example: docker run --volume new-init.sh:/init.sh:ro -COPY ./scripts/init/owncloud.sh /init.sh -RUN mkdir -p data; touch data/owncloud.log - -RUN composer install --no-dev -RUN make install-nodejs-deps - -USER root -RUN cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt -CMD /usr/sbin/apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow data/owncloud.log +RUN set -ex; \ + cd /usr/src/; \ + git clone \ + --depth 1 \ + --recursive \ + --shallow-submodules \ + --branch ${OWNCLOUD_BRANCH} \ + ${OWNCLOUD_REPO} \ + owncloud; \ + rm -rf /usr/src/owncloud/.git; \ + mkdir -p /usr/src/owncloud/data; \ + mkdir -p /usr/src/owncloud/custom_apps; \ + chmod +x /usr/src/owncloud/occ + +RUN cd /usr/src/owncloud; \ + composer install --no-dev; \ + make install-nodejs-deps + +# After cloning, `git` is no longer needed at runtime, so remove it to reduce image size. +RUN apt-get purge -y git && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY ./scripts/owncloud/*.sh / +COPY ./scripts/owncloud/upgrade.exclude / +COPY ./configs/owncloud/* /usr/src/owncloud/config/ + +ENTRYPOINT ["/entrypoint.sh"] +CMD apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow /var/www/html/data/owncloud.log diff --git a/docker/dockerfiles/revad.Dockerfile b/docker/dockerfiles/revad.Dockerfile index c346866c..be00fecf 100644 --- a/docker/dockerfiles/revad.Dockerfile +++ b/docker/dockerfiles/revad.Dockerfile @@ -1,86 +1,159 @@ -# stage 1: build stage -FROM golang:1.22.1-bookworm@sha256:d996c645c9934e770e64f05fc2bc103755197b43fd999b3aa5419142e1ee6d78 AS build - +# ---------------------------------------------------------------------------- +# Multi-stage build of revad from the cs3org/reva repository. +# This Dockerfile: +# 1. Builds revad from source using Go in a reproducible, pinned environment. +# 2. Creates a minimal runtime image with the revad binary, configurations, and certificates. +# +# ---------------------------------------------------------------------------- +# Stage 1: Build Stage +# ---------------------------------------------------------------------------- +# Use a specific, pinned Go image to ensure reproducible and secure builds. +FROM golang:1.23.4-bookworm@sha256:ef30001eeadd12890c7737c26f3be5b3a8479ccdcdc553b999c84879875a27ce AS build + +# Enable CGO for better performance on certain operations (e.g., SQLite). ENV CGO_ENABLED=1 -RUN apt-get update - -RUN DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --assume-yes \ - git \ - bash \ - make \ - build-essential \ - libsqlite3-dev - -# go to root directory. +# ---------------------------------------------------------------------------- +# Install Required Packages +# ---------------------------------------------------------------------------- +# Update package list and install build tools needed by revad and its dependencies. +# Use --no-install-recommends to avoid unnecessary packages and reduce image size. +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --assume-yes \ + git \ + bash \ + make \ + build-essential \ + libsqlite3-dev; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* + +# Set the working directory to root to have a clean starting point. WORKDIR / -# fetch revad from source. -ARG REPO_REVA=https://github.com/cs3org/reva -ARG BRANCH_REVA=v1.28.0 -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . -# $RANDOM returns random number each time. +# ---------------------------------------------------------------------------- +# Build Arguments +# ---------------------------------------------------------------------------- +# These allow customizing which repository and branch to clone at build time. +# CACHEBUST is used to force rebuild steps when needed. +ARG REVA_REPO=https://github.com/cs3org/reva +ARG REVA_BRANCH=v1.28.0 ARG CACHEBUST="default" -RUN git clone \ - --depth 1 \ - --branch ${BRANCH_REVA} \ - ${REPO_REVA} \ - reva-git -# change directory to reva git. +# ---------------------------------------------------------------------------- +# Clone Repository +# ---------------------------------------------------------------------------- +# Clone the specified branch of the OCM stub repository with minimal depth +# to reduce build time and image size. Also fetch submodules if present. +RUN git clone \ + --depth 1 \ + --recursive \ + --shallow-submodules \ + --branch ${REVA_BRANCH} \ + ${REVA_REPO} \ + /reva-git + +# ---------------------------------------------------------------------------- +# Set Working Directory +# ---------------------------------------------------------------------------- +# Set the working directory to the application directory. WORKDIR /reva-git -# copy and download dependencies. +# ---------------------------------------------------------------------------- +# Install Dependencies +# ---------------------------------------------------------------------------- +# Download Go module dependencies specified in go.mod to improve build caching. RUN go mod download -# only build revad, leave out reva and test and lint and docs. +# ---------------------------------------------------------------------------- +# Build Reva +# ---------------------------------------------------------------------------- +# Build the `revad` binary. +# Using `make revad` as per repository instructions. RUN make revad -# stage 2: app image. -FROM debian:bookworm@sha256:aadf411dc9ed5199bc7dab48b3e6ce18f8bbee4f170127f5ff1b75cd8035eb36 - -# keys for oci taken from: -# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys -LABEL org.opencontainers.image.licenses=MIT +# ---------------------------------------------------------------------------- +# Stage 2: Application Image +# ---------------------------------------------------------------------------- +# Use a minimal Debian-based image for the runtime environment. +FROM debian:bookworm-slim@sha256:1537a6a1cbc4b4fd401da800ee9480207e7dc1f23560c21259f681db56768f63 + +# ---------------------------------------------------------------------------- +# OCI Image Metadata +# ---------------------------------------------------------------------------- +# Provide metadata that describes this image, its source, and authorship. +LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.title="Pondersource Revad Image" LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" -# set the timezone and install CA certificates. -RUN apt-get update - -RUN DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --assume-yes \ - bash \ - curl \ - tzdata \ - iproute2 \ - ca-certificates - +# ---------------------------------------------------------------------------- +# Install Required Packages +# ---------------------------------------------------------------------------- +# Update package list and install: +# - bash: shell for scripts and operations. +# - curl: common utility for network operations. +# - tzdata: for time zone data, set to UTC for consistency. +# - iproute2: networking utilities that might be needed. +# - ca-certificates: to trust system certificates including custom ones. +# Use --no-install-recommends to avoid unnecessary packages and reduce image size. +RUN apt-get update; \ + DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --assume-yes \ + bash \ + curl \ + tzdata \ + iproute2 \ + ca-certificates; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* + +# Set timezone to UTC for consistent logging and operations. ENV TZ=Etc/UTC -# copy the binary from the build stage. -COPY --from=build /reva-git/cmd /reva-git/cmd +# Copy the pre-built `revad` binary and related tools from the build stage. +# The `revad` binary is found under /reva-git/cmd in the build stage. +COPY --from=build /reva-git/cmd /reva-git/cmd -# copy the reva config files from host. -COPY ./configs/revad /configs/revad +# Copy configuration files for revad into the container. +# These configurations will control revad behavior at runtime. +COPY ./configs/revad /configs/revad -# trust all the certificates: -COPY ./tls/certificates/reva* /tls/ -COPY ./tls/certificate-authority/* /tls/ -RUN ln -sf /tls/*.crt /usr/local/share/ca-certificates -RUN update-ca-certificates +# Copy TLS certificates from the host and trust them. +# This ensures revad can serve HTTPS or verify other services. +COPY ./tls/certificates/reva* /tls/ +COPY ./tls/certificate-authority/* /tls/ -RUN mkdir -p /var/tmp/reva/ +# Update the CA certificates store with newly added certificates. +RUN ln -sf /tls/*.crt /usr/local/share/ca-certificates; \ + update-ca-certificates -# update path to include revad bin directory. -ENV PATH="${PATH}:/reva/cmd/revad" +# Create necessary directories for runtime operations (e.g., logs, temp files). +RUN mkdir -p /var/tmp/reva/ -COPY ./scripts/reva/* /usr/bin/ +# Add the revad binary directory to PATH for convenience. +ENV PATH="${PATH}:/reva-git/cmd/revad" -RUN chmod +x /usr/bin/run.sh && chmod +x /usr/bin/kill.sh && chmod +x /usr/bin/entrypoint.sh +# Copy utility scripts (e.g., entrypoint, run, kill) into the container. +# Ensure these scripts have appropriate shebang lines and `chmod +x` done. +# These scripts are responsible for container lifecycle management. +COPY ./scripts/reva/* /usr/bin/ +RUN chmod +x /usr/bin/run.sh /usr/bin/kill.sh /usr/bin/entrypoint.sh +# ---------------------------------------------------------------------------- +# Entrypoint script. +# ---------------------------------------------------------------------------- +# Set the container entrypoint. This script can handle preparation steps before starting revad. ENTRYPOINT ["/usr/bin/entrypoint.sh"] -# keep Docker Container Running for Debugging. -CMD tail -F /var/log/revad.log +# ---------------------------------------------------------------------------- +# Startup Command +# ---------------------------------------------------------------------------- +# The default command is currently to follow the revad log. +CMD ["tail", "-F", "/var/log/revad.log"] + +# ---------------------------------------------------------------------------- +# Healthcheck +# ---------------------------------------------------------------------------- +# Check if the application responds on port 443. Using curl with -k to ignore TLS. +# HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ +# CMD curl -k -f http://localhost:... || exit 1 From f9ce6efa055fe26d1f7bb878ba67d28a0795041c Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 12:58:39 +0000 Subject: [PATCH 029/184] refactor: add functions and comments --- docker/tls/generate-certificate-authority.sh | 125 +++++++++-- docker/tls/generate-certificates.sh | 210 +++++++++++++------ 2 files changed, 252 insertions(+), 83 deletions(-) diff --git a/docker/tls/generate-certificate-authority.sh b/docker/tls/generate-certificate-authority.sh index e2ef80c2..ad1bf6bd 100755 --- a/docker/tls/generate-certificate-authority.sh +++ b/docker/tls/generate-certificate-authority.sh @@ -1,29 +1,112 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e +# ----------------------------------------------------------------------------------- +# Certificate Authority Generation Script +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script generates a private key and a self-signed certificate for a custom +# Certificate Authority (CA) in a Docker-based development environment. The resulting +# CA files (private key and certificate) can be used to sign certificates for various +# services within the environment, enabling secure, trusted TLS communication. +# +# Requirements: +# - OpenSSL must be installed and available in PATH. +# +# Notes: +# - The CA private key and certificate are stored in the "certificate-authority" directory. +# - The certificate is set to expire in ~100 years (36500 days), which is convenient +# for long-lived development environments without worrying about frequent renewals. +# +# Example: +# ./generate-ca.sh +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to inability to generate keys, certificates, or navigate directories. +# +# ----------------------------------------------------------------------------------- -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) +# Halt on errors and treat pipeline failures as errors +set -e +set -o pipefail -cd "${DIR}" || exit +# ----------------------------------------------------------------------------------- +# Function: get_script_dir +# Purpose: Resolve the directory where the script is located, even if it is a symlink. +# Returns: The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +get_script_dir() { + local source="${BASH_SOURCE[0]}" + while [[ -L "${source}" ]]; do + local dir + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd +} +# ----------------------------------------------------------------------------------- +# Initialize Environment Variables +# ----------------------------------------------------------------------------------- +DIR=$(get_script_dir) +cd "${DIR}" || { echo "Error: Unable to navigate to script directory." >&2; exit 1; } ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} +export ENV_ROOT + +# Directory for storing CA files +CA_DIR="${ENV_ROOT}/certificate-authority" + +# ----------------------------------------------------------------------------------- +# Function: generate_ca +# Purpose: Generate the Certificate Authority private key and self-signed certificate. +# Behavior: +# 1. Ensures the CA directory exists. +# 2. Generates a 2048-bit RSA private key. +# 3. Creates a self-signed certificate valid for 36500 days (~100 years). +# +# On Error: +# - Prints an error message and exits with code 1. +# ----------------------------------------------------------------------------------- +generate_ca() { + # Ensure the CA directory exists + mkdir -p "${CA_DIR}" + + # Check that openssl is available + if ! command -v openssl >/dev/null 2>&1; then + echo "Error: OpenSSL is not installed or not in PATH." >&2 + exit 1 + fi + + # Generate the CA private key + printf "Generating Certificate Authority private key...\n" + if ! openssl genrsa -out "${CA_DIR}/dev-stock.key" 2048; then + echo "Error: Failed to generate CA private key." >&2 + exit 1 + fi + + # Generate the self-signed CA certificate + printf "Generating self-signed Certificate Authority certificate...\n" + if ! openssl req -new -x509 \ + -days 36500 \ + -key "${CA_DIR}/dev-stock.key" \ + -out "${CA_DIR}/dev-stock.crt" \ + -subj "/C=RO/ST=Bucharest/L=Bucharest/O=IT/CN=dev-stock"; then + echo "Error: Failed to generate CA certificate." >&2 + exit 1 + fi + + # Print success message with file locations + printf "\nCertificate Authority setup complete.\n" + printf "Private Key: %s/dev-stock.key\n" "${CA_DIR}" + printf "Certificate: %s/dev-stock.crt\n" "${CA_DIR}" +} -echo "Generating Certificate Authority key" -openssl genrsa -out "${ENV_ROOT}/certificate-authority/dev-stock.key" +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- -echo "Generate CA self-signed certificate" -openssl req -new -x509 \ - -days 36500 \ - -key "${ENV_ROOT}/certificate-authority/dev-stock.key" \ - -out "${ENV_ROOT}/certificate-authority/dev-stock.crt" \ - -subj "/C=RO/ST=Bucharest/L=Bucharest/O=IT/CN=dev-stock" +# Start the CA generation process +generate_ca diff --git a/docker/tls/generate-certificates.sh b/docker/tls/generate-certificates.sh index 0da66ed2..7a0a7461 100755 --- a/docker/tls/generate-certificates.sh +++ b/docker/tls/generate-certificates.sh @@ -1,76 +1,162 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e +# ----------------------------------------------------------------------------------- +# Certificate Generation Script for Development Environments +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) +# Description: +# This script generates self-signed certificates for various services in a Docker-based +# development environment. The certificates are signed using a custom Certificate +# Authority (CA) provided in the "certificate-authority" directory. It supports +# generating certificates for multiple EFSS (Enterprise File Synchronization and Sharing) +# instances and associated services (Reva, WOPI), as well as standalone services like +# mesh directories and identity providers (idp). +# +# Requirements: +# - OpenSSL must be installed and available in PATH. +# - A custom CA must be present in the "certificate-authority" directory. +# Files required: "dev-stock.crt" and "dev-stock.key". +# +# Behavior: +# - The script removes and recreates the "certificates" directory at each run, ensuring a +# clean state. +# - Certificates are generated with a 100-year validity (36500 days) for convenience in +# long-term development environments. +# - Ownership of "idp" certificates is adjusted to user "1000:root" to meet specific service +# requirements. +# +# Notes: +# - The script assumes a ".docker" domain is used for all services (e.g., "idp.docker", +# "owncloud1.docker"). +# - Add or remove services or EFSS instances by modifying the arrays and loops below. +# +# Example: +# ./generate-certificates.sh +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to missing directories, CA files, or certificate signing issues. + +# ----------------------------------------------------------------------------------- +# Safety and Error Handling +# ----------------------------------------------------------------------------------- + +# Halt on any errors and treat pipeline failures as errors +set -e +set -o pipefail -cd "${DIR}" || exit +# ----------------------------------------------------------------------------------- +# Function to Resolve Script Location +# ----------------------------------------------------------------------------------- +# Resolves the directory where the script is located, even if it is a symlink. +get_script_dir() { + local source=${BASH_SOURCE[0]} + while [[ -L "${source}" ]]; do + local dir; dir=$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd) + source=$(readlink "${source}") + [[ "${source}" != /* ]] && source="${dir}/${source}" # Resolve relative symlinks + done + cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd +} +# Set the environment root to the directory containing this script +DIR=$(get_script_dir) +cd "${DIR}" || { echo "Error: Unable to navigate to script directory." >&2; exit 1; } ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -function createCertificate { - echo "Generating key and CSR for ${1}.docker" - - openssl req -new -nodes \ - -out "${ENV_ROOT}/certificates/${1}.csr" \ - -keyout "${ENV_ROOT}/certificates/${1}.key" \ - -subj "/C=RO/ST=Bucharest/L=Bucharest/O=IT/CN=${1}.docker" - - echo Creating extfile - echo "subjectAltName = @alt_names" > "${ENV_ROOT}/certificates/${1}.cnf" - echo "[alt_names]" >> "${ENV_ROOT}/certificates/${1}.cnf" - echo "DNS.1 = ${1}.docker" >> "${ENV_ROOT}/certificates/${1}.cnf" - - echo "Signing CSR for ${1}.docker, creating cert." - - openssl x509 -req \ - -days 36500 \ - -in "${ENV_ROOT}/certificates/${1}.csr" \ - -CA "${ENV_ROOT}/certificate-authority/dev-stock.crt" \ - -CAkey "${ENV_ROOT}/certificate-authority/dev-stock.key" \ - -CAcreateserial \ - -out "${ENV_ROOT}/certificates/${1}.crt" \ - -extfile "${ENV_ROOT}/certificates/${1}.cnf" +export ENV_ROOT + +# ----------------------------------------------------------------------------------- +# Function to Generate Certificates +# ----------------------------------------------------------------------------------- +# This function generates a self-signed certificate and private key for a given hostname. +# +# Arguments: +# 1. Hostname (e.g., "idp", "owncloud1") +# +# Process: +# - Generates a private key and CSR (Certificate Signing Request) for the given hostname. +# - Creates a configuration file specifying subjectAltName for the hostname. +# - Uses the CA (dev-stock.crt and dev-stock.key) to sign the CSR, producing a .crt file. +# - If the hostname is "idp", adjusts ownership of the generated files. +create_certificate() { + local hostname=$1 + printf "Generating key and CSR for %s.docker\n" "${hostname}" + + # Ensure the certificates directory exists + mkdir -p "${ENV_ROOT}/certificates" + + # Check if CA files exist + if [[ ! -f "${ENV_ROOT}/certificate-authority/dev-stock.crt" || ! -f "${ENV_ROOT}/certificate-authority/dev-stock.key" ]]; then + printf "Error: CA files not found. Expected dev-stock.crt and dev-stock.key in certificate-authority.\n" >&2 + exit 1 + fi + + # Generate the private key and CSR + openssl req -new -nodes \ + -out "${ENV_ROOT}/certificates/${hostname}.csr" \ + -keyout "${ENV_ROOT}/certificates/${hostname}.key" \ + -subj "/C=RO/ST=Bucharest/L=Bucharest/O=IT/CN=${hostname}.docker" + + # Create a configuration file for subjectAltName + printf "Creating extfile for %s.docker\n" "${hostname}" + cat > "${ENV_ROOT}/certificates/${hostname}.cnf" <&2 + exit 1 + fi + + # Adjust ownership of generated files for "idp" + if [[ "${hostname}" == "idp" ]]; then + printf "Changing ownership for %s certificates.\n" "${hostname}" + sudo chown 1000:root "${ENV_ROOT}/certificates/${hostname}."* + fi } +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +# Clean up and recreate the certificates directory rm -rf "${ENV_ROOT}/certificates" mkdir -p "${ENV_ROOT}/certificates" -createCertificate idp -sudo chown 1000:root "${ENV_ROOT}"/certificates/idp.* - -createCertificate meshdir -createCertificate revad1 -createCertificate revad2 - -for efss in owncloud nextcloud cernbox; do - createCertificate ${efss}1 - createCertificate ${efss}2 - createCertificate ${efss}3 - createCertificate ${efss}4 - createCertificate reva${efss}1 - createCertificate reva${efss}2 - createCertificate reva${efss}3 - createCertificate reva${efss}4 - createCertificate wopi${efss}1 - createCertificate wopi${efss}2 - createCertificate wopi${efss}3 - createCertificate wopi${efss}4 +# Generate certificates for standalone services +create_certificate idp +create_certificate meshdir +create_certificate revad1 +create_certificate revad2 + +# Generate certificates for multiple EFSS (Enterprise File Sync and Share) instances +efss_list=("owncloud" "nextcloud" "cernbox") +for efss in "${efss_list[@]}"; do + for i in {1..4}; do + create_certificate "${efss}${i}" # EFSS instance + create_certificate "reva${efss}${i}" # Reva service for EFSS + create_certificate "wopi${efss}${i}" # WOPI service for EFSS + done done -for efss in seafile ocis ocmstub; do - createCertificate ${efss}1 - createCertificate ${efss}2 - createCertificate ${efss}3 - createCertificate ${efss}4 +# Generate certificates for additional EFSS instances +additional_efss=("seafile" "ocis" "ocmstub") +for efss in "${additional_efss[@]}"; do + for i in {1..4}; do + create_certificate "${efss}${i}" + done done + +printf "All certificates generated successfully.\n" From 4136a5e380c3480ad65252dcb93252048ddb9928 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 13:00:06 +0000 Subject: [PATCH 030/184] [no ci] refactor: share with test runner with documentation and more modular fucntions --- .../share-with/nextcloud-nextcloud.sh | 638 ++++++++++------ .../share-with/nextcloud-owncloud.sh | 683 +++++++++++------ .../share-with/owncloud-nextcloud.sh | 685 ++++++++++++------ .../share-with/owncloud-owncloud.sh | 625 ++++++++++------ 4 files changed, 1746 insertions(+), 885 deletions(-) diff --git a/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh index e2e3b9ae..fe931ddb 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh @@ -1,41 +1,159 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.12 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.12 -EFSS_PLATFORM_2_VERSION=${2:-"v27.1.11"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to Nextcloud OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-nextcloud.sh v28.0.14 v27.1.11 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_VERSION="v27.1.11" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containerS +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { if [ "${SCRIPT_MODE}" = "ci" ]; then "$@" >/dev/null 2>&1 else @@ -43,196 +161,266 @@ function redirect_to_null_cmd() { fi } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi } -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create Nextcloud containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://nextcloud2.docker (username: michiel, password: dejong)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# Nextclouds. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" -createEfss nextcloud 2 michiel dejong nextcloud.sh "${EFSS_PLATFORM_2_VERSION}" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://nextcloud2.docker -> username: michiel password: dejong" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh index 78656ec9..746ee350 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh @@ -1,40 +1,160 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to ownCloud OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, ownCloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v10.15.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-owncloud.sh v28.0.14 v10.15.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v10.15.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containerS +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_2_VERSION=${2:-"v10.15.0"} +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} -function redirect_to_null_cmd() { +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { if [ "${SCRIPT_MODE}" = "ci" ]; then "$@" >/dev/null 2>&1 else @@ -42,198 +162,319 @@ function redirect_to_null_cmd() { fi } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_owncloud +# Purpose: Create a ownCloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_owncloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaowncloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaowncloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="owncloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="owncloud${number}" \ + -e OWNCLOUD_HOST="owncloud${number}.docker" \ + -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ + -e OWNCLOUD_ADMIN_USER="${user}" \ + -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ + -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="mariaowncloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi } -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://owncloud1.docker (username: marie, password: radioactivity)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sm-ocm.sh" "${ENV_ROOT}/temp/owncloud.sh" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -############ -### EFSS ### -############ - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownCloud. -createEfss owncloud 1 marie radioactivity owncloud.sh latest ocm-test-suite - -# Nextcloud. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh index eff36fff..8a8efbed 100755 --- a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh @@ -1,239 +1,480 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_2_VERSION=${2:-"v28.0.14"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" +# ----------------------------------------------------------------------------------- +# Script to Test ownCloud to Nextcloud OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, ownCloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./owncloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v10.15.0"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./owncloud-nextcloud.sh v10.15.0 v28.0.14 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v10.15.0" +DEFAULT_EFSS_2_VERSION="v27.1.11" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containerS +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." fi + done } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) done - redirect_to_null_cmd echo "${1} port ${2} is open" + run_quietly_if_ci echo "Port ${port} is now open on ${container}." } -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 else - local image="pondersource/dev-stock-${platform}-${image}" + "$@" fi +} - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sm-ocm.sh" "${ENV_ROOT}/temp/owncloud.sh" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -############ -### EFSS ### -############ - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownCloud. -createEfss owncloud 1 marie radioactivity owncloud.sh latest ocm-test-suite - -# Nextcloud. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_2_VERSION}" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/owncloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_owncloud +# Purpose: Create a ownCloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_owncloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaowncloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaowncloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="owncloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="owncloud${number}" \ + -e OWNCLOUD_HOST="owncloud${number}.docker" \ + -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ + -e OWNCLOUD_ADMIN_USER="${user}" \ + -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ + -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="mariaowncloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://owncloud1.docker (username: marie, password: radioactivity)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/share-with/owncloud-owncloud.sh b/dev/ocm-test-suite/share-with/owncloud-owncloud.sh index d5b809fa..0ea4621a 100755 --- a/dev/ocm-test-suite/share-with/owncloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-owncloud.sh @@ -1,39 +1,159 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} +# ----------------------------------------------------------------------------------- +# Script to Test ownCloud to ownCloud OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as ownCloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./owncloud-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./owncloud-owncloud.sh v10.15.0 v10.15.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_VERSION="v10.15.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containerS +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_2_VERSION=${2:-"v10.15.0"} +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} -function redirect_to_null_cmd() { +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { if [ "${SCRIPT_MODE}" = "ci" ]; then "$@" >/dev/null 2>&1 else @@ -41,195 +161,266 @@ function redirect_to_null_cmd() { fi } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_owncloud +# Purpose: Create a ownCloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_owncloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaowncloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaowncloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="owncloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="owncloud${number}" \ + -e OWNCLOUD_HOST="owncloud${number}.docker" \ + -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ + -e OWNCLOUD_ADMIN_USER="${user}" \ + -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ + -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="mariaowncloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" } -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create ownCloud containers + # # id # username # password # image # tag + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 2 "mahdi" "baghbani" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://owncloud1.docker (username: marie, password: radioactivity)" + echo " https://owncloud2.docker (username: mahdi, password: baghbani)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/owncloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sm-ocm.sh" "${ENV_ROOT}/temp/owncloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################ -### ownCloud ### -################ - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownClouds. -createEfss owncloud 1 marie radioactivity owncloud.sh latest ocm-test-suite -createEfss owncloud 2 mahdi baghbani owncloud.sh latest ocm-test-suite - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://nextcloud2.docker -> username: michiel password: dejong" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/owncloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From 428b181e89ce83aed69a24a05025365920dfed94 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 13:01:10 +0000 Subject: [PATCH 031/184] [no ci] refactor: add comments and modular functions --- docker/scripts/entrypoint.sh | 110 ++++++++++++++++++++-- docker/scripts/init/seafile.sh | 161 +++++++++++++++++++++++++++++++-- 2 files changed, 252 insertions(+), 19 deletions(-) diff --git a/docker/scripts/entrypoint.sh b/docker/scripts/entrypoint.sh index 514cd178..8529ee5c 100755 --- a/docker/scripts/entrypoint.sh +++ b/docker/scripts/entrypoint.sh @@ -1,17 +1,109 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e +# ----------------------------------------------------------------------------------- +# Docker Initialization Script for TLS Certificates +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- +# Description: +# This script runs inside a Docker container to set up TLS certificates for a given +# host. It copies certificates and keys from a specified directory, validates their +# presence, and creates symbolic links for easy reference by the main service. +# Finally, it executes the provided command (e.g., the container's CMD). +# +# Requirements: +# - The HOST environment variable must be set. +# - The /certificates directory should contain .crt and .key files matching the HOST. +# +# Usage: +# In a Dockerfile: +# COPY entrypoint.sh /entrypoint.sh +# ENTRYPOINT ["/entrypoint.sh"] +# +# The container's CMD will be executed by this script once TLS setup is complete. +# +# Notes: +# - If HOST is "example.com", then the script expects to find "example.com.crt" and +# "example.com.key" inside the /tls directory after copying from /certificates. +# - The script creates /tls directory if it doesn't exist. +# - Symbolic links are created: +# /tls/server.crt -> /tls/${HOST}.crt +# /tls/server.key -> /tls/${HOST}.key +# +# Example: +# HOST=example.com docker run --rm -e HOST=example.com myimage:latest +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to missing HOST, missing certificates, or command execution issues. + +# ----------------------------------------------------------------------------------- +# Safety and Error Handling +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# Define a trap to clean up resources on script exit or error. +# This ensures the /tls directory is removed if something goes wrong. +trap 'rm -rf /tls' EXIT + +# ----------------------------------------------------------------------------------- +# Directory and Certificate Setup +# ----------------------------------------------------------------------------------- + +# Ensure the /tls directory exists mkdir -p /tls -[ -d "/certificates" ] && \ - cp -f /certificates/*.crt /tls/ \ - && \ - cp -f /certificates/*.key /tls/ +# If the /certificates directory exists, copy .crt and .key files to /tls +if [[ -d "/certificates" ]]; then + printf "Copying certificates and keys to /tls...\n" + # Copy .crt files + find /certificates -type f -name "*.crt" -exec cp -f {} /tls/ \; + + # Copy .key files + find /certificates -type f -name "*.key" -exec cp -f {} /tls/ \; + + printf "Certificate and key files copied successfully.\n" +else + printf "Warning: /certificates directory does not exist. Skipping certificate copy.\n" >&2 +fi + +# ----------------------------------------------------------------------------------- +# Validate HOST Environment Variable +# ----------------------------------------------------------------------------------- + +# Ensure the HOST environment variable is set +if [[ -z "${HOST}" ]]; then + printf "Error: HOST environment variable is not set. Aborting.\n" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Check for HOST-Specific Certificate and Key +# ----------------------------------------------------------------------------------- +crt_path="/tls/${HOST}.crt" +key_path="/tls/${HOST}.key" + +if [[ -f "${crt_path}" && -f "${key_path}" ]]; then + printf "Creating symbolic links for certificates...\n" + # Create symbolic links to /tls/server.crt and /tls/server.key + ln --symbolic --force "${crt_path}" /tls/server.crt + ln --symbolic --force "${key_path}" /tls/server.key + + printf "Symbolic links created successfully.\n" +else + printf "Error: Certificate or key file for host '%s' not found in /tls.\n" "${HOST}" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Execute the Provided Command +# ----------------------------------------------------------------------------------- -ln --symbolic --force "/tls/${HOST}.crt" /tls/server.crt -ln --symbolic --force "/tls/${HOST}.key" /tls/server.key +# Print the command about to be executed for clarity +printf "Executing command: %s\n" "$*" -# This will exec the CMD from your Dockerfile, i.e. "npm start" +# Execute the provided command with exec so that it becomes the container's main process exec "$@" diff --git a/docker/scripts/init/seafile.sh b/docker/scripts/init/seafile.sh index a05546cf..ad3acbba 100755 --- a/docker/scripts/init/seafile.sh +++ b/docker/scripts/init/seafile.sh @@ -1,24 +1,165 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. +# ----------------------------------------------------------------------------------- +# Seafile Seahub Settings Configuration Script +# ----------------------------------------------------------------------------------- +# Description: +# This script updates the Seafile Seahub settings to: +# 1. Enable and configure Open Cloud Mesh (OCM) integration with a unique provider UUID. +# 2. Update Memcached settings based on environment variables or defaults. +# +# Requirements: +# - Seahub settings file must exist at the specified SEAHUB_SETTINGS path. +# - The user running this script must have write permissions to the Seahub settings file. +# +# Environment Variables: +# SEAFILE_MEMCACHE_HOST (optional): Hostname for Memcached (default: "memcached") +# SEAFILE_MEMCACHE_PORT (optional): Port for Memcached (default: "11211") +# +# Arguments: +# 1 (optional): Remote server name for OCM (default: "seafile") +# +# Notes: +# - A unique UUID is generated from /proc/sys/kernel/random/uuid for OCM. +# - Modifications are appended to the Seahub settings file. Existing settings are not removed. +# +# Example: +# ./seafile.sh "myserver" +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to missing files, permissions, or command errors. +# +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Exit on Errors and Ensure Robust Pipeline Handling +# ----------------------------------------------------------------------------------- set -e +set -o pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Environment Variables +# ----------------------------------------------------------------------------------- +SEAHUB_SETTINGS="/opt/seafile/conf/seahub_settings.py" # Path to Seahub settings file + +# Default values for Memcached host and port +SEAFILE_MEMCACHE_HOST="${SEAFILE_MEMCACHE_HOST:-memcached}" +SEAFILE_MEMCACHE_PORT="${SEAFILE_MEMCACHE_PORT:-11211}" + +# Default remote server name for OCM (can be overridden by script argument) +DEFAULT_REMOTE_SERVER="seafile" + +# ----------------------------------------------------------------------------------- +# Function: generate_uuid +# Purpose: Generate a UUID using the kernel's random generator. +# Returns: UUID as a string. +# On Error: Prints an error and exits with code 1 if the UUID file is not found. +# ----------------------------------------------------------------------------------- +generate_uuid() { + local uuid_file="/proc/sys/kernel/random/uuid" + if [[ -f "${uuid_file}" ]]; then + cat "${uuid_file}" + else + echo "Error: UUID generator file not found at ${uuid_file}." >&2 + exit 1 + fi +} -uuid=$(cat /proc/sys/kernel/random/uuid) +# ----------------------------------------------------------------------------------- +# Function: append_ocm_configuration +# Purpose: Append OCM configuration to the Seahub settings file. +# Arguments: +# 1. settings_file: Path to the Seahub settings file +# 2. uuid: Unique provider ID for OCM +# 3. remote_server: Remote server name for OCM +# On Error: Prints an error and exits if the file is not writable. +# ----------------------------------------------------------------------------------- +append_ocm_configuration() { + local settings_file="$1" + local uuid="$2" + local remote_server="$3" -# not the best way to do this, I know. -remote_server_1=${1-seafile} + # Verify write permission for the settings file + if [[ ! -w "${settings_file}" ]]; then + echo "Error: Cannot write to ${settings_file}. Check file permissions." >&2 + exit 1 + fi -cat >> /opt/seafile/conf/seahub_settings.py <> "${settings_file}" <&2 + exit 1 + fi + + printf "Memcached configuration updated successfully to %s:%s\n" "${memcache_host}" "${memcache_port}" +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Verify Seahub settings file exists + if [[ ! -f "${SEAHUB_SETTINGS}" ]]; then + echo "Error: Seahub settings file not found at ${SEAHUB_SETTINGS}" >&2 + exit 1 + fi + + # Generate a UUID for the OCM provider + printf "Generating UUID for OCM provider...\n" + uuid=$(generate_uuid) + printf "Generated UUID: %s\n" "${uuid}" + + # Parse input argument for remote server name + # If no argument provided, use DEFAULT_REMOTE_SERVER + remote_server=${1:-"${DEFAULT_REMOTE_SERVER}"} + printf "Using remote server: %s\n" "${remote_server}" + + # Append OCM configuration to Seahub settings + append_ocm_configuration "${SEAHUB_SETTINGS}" "${uuid}" "${remote_server}" + + # Update memcached configuration + update_memcached_configuration "${SEAHUB_SETTINGS}" "${SEAFILE_MEMCACHE_HOST}" "${SEAFILE_MEMCACHE_PORT}" + + printf "Seafile Seahub configuration completed successfully.\n" +} + +# Execute the main function +main "$@" From 75a775b8938f6d5321c8117dfb76314abefe14ef Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 13:06:38 +0000 Subject: [PATCH 032/184] [no ci] add: certificates to the image and update os trusted certs --- docker/dockerfiles/ocmstub.Dockerfile | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docker/dockerfiles/ocmstub.Dockerfile b/docker/dockerfiles/ocmstub.Dockerfile index a5dcc203..15907777 100644 --- a/docker/dockerfiles/ocmstub.Dockerfile +++ b/docker/dockerfiles/ocmstub.Dockerfile @@ -22,8 +22,9 @@ LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" # - git: required to clone the repository # Use --no-install-recommends to avoid unnecessary packages and reduce image size. RUN apt-get update && apt-get install --no-install-recommends --assume-yes \ + git \ iproute2 \ - git; \ + ca-certificates; \ apt-get clean && rm -rf /var/lib/apt/lists/* # ---------------------------------------------------------------------------- @@ -65,6 +66,16 @@ WORKDIR /ocmstub # --production ensures only production dependencies are installed, reducing size. RUN npm ci --production +# ---------------------------------------------------------------------------- +# Install TLS Certificates +# ---------------------------------------------------------------------------- +# Copy self signed certificates and link them to OS cert directory and update +# the systems trusted certificates +COPY ./tls/certificates/* /tls/ +COPY ./tls/certificate-authority/* /tls/ +RUN ln --symbolic --force /tls/*.crt /usr/local/share/ca-certificates; \ + update-ca-certificates + # ---------------------------------------------------------------------------- # Expose Ports # ---------------------------------------------------------------------------- From 6b12c9b43d2a1602fbb5dc97c9e1ee662b152173 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 13:40:33 +0000 Subject: [PATCH 033/184] [no ci] add: new scripts and configs for nextclouyd and owncloud images --- .../nextcloud/apache-pretty-urls.config.php | 4 + docker/configs/nextcloud/apache.conf | 41 +++ docker/configs/nextcloud/apcu.config.php | 4 + docker/configs/nextcloud/apps.config.php | 15 + docker/configs/nextcloud/autoconfig.php | 41 +++ docker/configs/nextcloud/redis.config.php | 17 + .../nextcloud/reverse-proxy.config.php | 35 ++ docker/configs/nextcloud/s3.config.php | 48 +++ docker/configs/nextcloud/smtp.config.php | 22 ++ docker/configs/nextcloud/swift.config.php | 31 ++ .../nextcloud/upgrade-disable-web.config.php | 4 + .../owncloud/apache-pretty-urls.config.php | 4 + docker/configs/owncloud/apache.conf | 41 +++ docker/configs/owncloud/apcu.config.php | 4 + docker/configs/owncloud/apps.config.php | 15 + docker/configs/owncloud/autoconfig.php | 41 +++ docker/configs/owncloud/redis.config.php | 17 + .../configs/owncloud/reverse-proxy.config.php | 35 ++ docker/configs/owncloud/s3.config.php | 48 +++ docker/configs/owncloud/smtp.config.php | 22 ++ docker/configs/owncloud/swift.config.php | 31 ++ .../owncloud/upgrade-disable-web.config.php | 4 + docker/scripts/nextcloud/cron.sh | 4 + docker/scripts/nextcloud/entrypoint.sh | 121 +++++++ docker/scripts/nextcloud/init.sh | 302 +++++++++++++++++ docker/scripts/nextcloud/upgrade.exclude | 6 + docker/scripts/owncloud/entrypoint.sh | 121 +++++++ docker/scripts/owncloud/init.sh | 303 ++++++++++++++++++ 28 files changed, 1381 insertions(+) create mode 100644 docker/configs/nextcloud/apache-pretty-urls.config.php create mode 100644 docker/configs/nextcloud/apache.conf create mode 100644 docker/configs/nextcloud/apcu.config.php create mode 100644 docker/configs/nextcloud/apps.config.php create mode 100644 docker/configs/nextcloud/autoconfig.php create mode 100644 docker/configs/nextcloud/redis.config.php create mode 100644 docker/configs/nextcloud/reverse-proxy.config.php create mode 100644 docker/configs/nextcloud/s3.config.php create mode 100644 docker/configs/nextcloud/smtp.config.php create mode 100644 docker/configs/nextcloud/swift.config.php create mode 100644 docker/configs/nextcloud/upgrade-disable-web.config.php create mode 100644 docker/configs/owncloud/apache-pretty-urls.config.php create mode 100644 docker/configs/owncloud/apache.conf create mode 100644 docker/configs/owncloud/apcu.config.php create mode 100644 docker/configs/owncloud/apps.config.php create mode 100644 docker/configs/owncloud/autoconfig.php create mode 100644 docker/configs/owncloud/redis.config.php create mode 100644 docker/configs/owncloud/reverse-proxy.config.php create mode 100644 docker/configs/owncloud/s3.config.php create mode 100644 docker/configs/owncloud/smtp.config.php create mode 100644 docker/configs/owncloud/swift.config.php create mode 100644 docker/configs/owncloud/upgrade-disable-web.config.php create mode 100755 docker/scripts/nextcloud/cron.sh create mode 100755 docker/scripts/nextcloud/entrypoint.sh create mode 100755 docker/scripts/nextcloud/init.sh create mode 100644 docker/scripts/nextcloud/upgrade.exclude create mode 100755 docker/scripts/owncloud/entrypoint.sh create mode 100755 docker/scripts/owncloud/init.sh diff --git a/docker/configs/nextcloud/apache-pretty-urls.config.php b/docker/configs/nextcloud/apache-pretty-urls.config.php new file mode 100644 index 00000000..72da1d8c --- /dev/null +++ b/docker/configs/nextcloud/apache-pretty-urls.config.php @@ -0,0 +1,4 @@ + '/', +); diff --git a/docker/configs/nextcloud/apache.conf b/docker/configs/nextcloud/apache.conf new file mode 100644 index 00000000..8eb259c5 --- /dev/null +++ b/docker/configs/nextcloud/apache.conf @@ -0,0 +1,41 @@ + + DocumentRoot /var/www/html + ServerName ${NEXTCLOUD_HOST} + Redirect permanent / https://${NEXTCLOUD_HOST}/ + + LogLevel warn + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + + + DocumentRoot /var/www/html + ServerName ${NEXTCLOUD_HOST} + + Protocols h2 http/1.1 + + LogLevel ${NEXTCLOUD_APACHE_LOGLEVEL} + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + Header always set Strict-Transport-Security "max-age=63072000;" + + SSLEngine on + SSLCertificateFile "/tls/server.crt" + SSLCertificateKeyFile "/tls/server.key" + + + Require all granted + Options FollowSymlinks MultiViews + AllowOverride All + + + Dav off + + + SetEnv HOME /var/www/html/ + SetEnv HTTP_HOME /var/www/html/ + Satisfy Any + + diff --git a/docker/configs/nextcloud/apcu.config.php b/docker/configs/nextcloud/apcu.config.php new file mode 100644 index 00000000..69fed876 --- /dev/null +++ b/docker/configs/nextcloud/apcu.config.php @@ -0,0 +1,4 @@ + '\OC\Memcache\APCu', +); diff --git a/docker/configs/nextcloud/apps.config.php b/docker/configs/nextcloud/apps.config.php new file mode 100644 index 00000000..4c37f72a --- /dev/null +++ b/docker/configs/nextcloud/apps.config.php @@ -0,0 +1,15 @@ + array ( + 0 => array ( + 'path' => OC::$SERVERROOT.'/apps', + 'url' => '/apps', + 'writable' => false, + ), + 1 => array ( + 'path' => OC::$SERVERROOT.'/custom_apps', + 'url' => '/custom_apps', + 'writable' => true, + ), + ), +); diff --git a/docker/configs/nextcloud/autoconfig.php b/docker/configs/nextcloud/autoconfig.php new file mode 100644 index 00000000..92ad2a1c --- /dev/null +++ b/docker/configs/nextcloud/autoconfig.php @@ -0,0 +1,41 @@ + '\OC\Memcache\Redis', + 'memcache.locking' => '\OC\Memcache\Redis', + 'redis' => array( + 'host' => getenv('REDIS_HOST'), + 'password' => getenv('REDIS_HOST_PASSWORD_FILE') ? trim(file_get_contents(getenv('REDIS_HOST_PASSWORD_FILE'))) : (string) getenv('REDIS_HOST_PASSWORD'), + ), + ); + + if (getenv('REDIS_HOST_PORT') !== false) { + $CONFIG['redis']['port'] = (int) getenv('REDIS_HOST_PORT'); + } elseif (getenv('REDIS_HOST')[0] != '/') { + $CONFIG['redis']['port'] = 6379; + } +} diff --git a/docker/configs/nextcloud/reverse-proxy.config.php b/docker/configs/nextcloud/reverse-proxy.config.php new file mode 100644 index 00000000..30c660ff --- /dev/null +++ b/docker/configs/nextcloud/reverse-proxy.config.php @@ -0,0 +1,35 @@ + array( + 'class' => '\OC\Files\ObjectStore\S3', + 'arguments' => array( + 'bucket' => getenv('OBJECTSTORE_S3_BUCKET'), + 'region' => getenv('OBJECTSTORE_S3_REGION') ?: '', + 'hostname' => getenv('OBJECTSTORE_S3_HOST') ?: '', + 'port' => getenv('OBJECTSTORE_S3_PORT') ?: '', + 'storageClass' => getenv('OBJECTSTORE_S3_STORAGE_CLASS') ?: '', + 'objectPrefix' => getenv("OBJECTSTORE_S3_OBJECT_PREFIX") ? getenv("OBJECTSTORE_S3_OBJECT_PREFIX") : "urn:oid:", + 'autocreate' => strtolower($autocreate) !== 'false', + 'use_ssl' => strtolower($use_ssl) !== 'false', + // required for some non Amazon S3 implementations + 'use_path_style' => $use_path == true && strtolower($use_path) !== 'false', + // required for older protocol versions + 'legacy_auth' => $use_legacyauth == true && strtolower($use_legacyauth) !== 'false' + ) + ) + ); + + if (getenv('OBJECTSTORE_S3_KEY_FILE')) { + $CONFIG['objectstore']['arguments']['key'] = trim(file_get_contents(getenv('OBJECTSTORE_S3_KEY_FILE'))); + } elseif (getenv('OBJECTSTORE_S3_KEY')) { + $CONFIG['objectstore']['arguments']['key'] = getenv('OBJECTSTORE_S3_KEY'); + } else { + $CONFIG['objectstore']['arguments']['key'] = ''; + } + + if (getenv('OBJECTSTORE_S3_SECRET_FILE')) { + $CONFIG['objectstore']['arguments']['secret'] = trim(file_get_contents(getenv('OBJECTSTORE_S3_SECRET_FILE'))); + } elseif (getenv('OBJECTSTORE_S3_SECRET')) { + $CONFIG['objectstore']['arguments']['secret'] = getenv('OBJECTSTORE_S3_SECRET'); + } else { + $CONFIG['objectstore']['arguments']['secret'] = ''; + } + + if (getenv('OBJECTSTORE_S3_SSE_C_KEY_FILE')) { + $CONFIG['objectstore']['arguments']['sse_c_key'] = trim(file_get_contents(getenv('OBJECTSTORE_S3_SSE_C_KEY_FILE'))); + } elseif (getenv('OBJECTSTORE_S3_SSE_C_KEY')) { + $CONFIG['objectstore']['arguments']['sse_c_key'] = getenv('OBJECTSTORE_S3_SSE_C_KEY'); + } +} diff --git a/docker/configs/nextcloud/smtp.config.php b/docker/configs/nextcloud/smtp.config.php new file mode 100644 index 00000000..66a2ef7e --- /dev/null +++ b/docker/configs/nextcloud/smtp.config.php @@ -0,0 +1,22 @@ + 'smtp', + 'mail_smtphost' => getenv('SMTP_HOST'), + 'mail_smtpport' => getenv('SMTP_PORT') ?: (getenv('SMTP_SECURE') ? 465 : 25), + 'mail_smtpsecure' => getenv('SMTP_SECURE') ?: '', + 'mail_smtpauth' => getenv('SMTP_NAME') && (getenv('SMTP_PASSWORD') || getenv('SMTP_PASSWORD_FILE')), + 'mail_smtpauthtype' => getenv('SMTP_AUTHTYPE') ?: 'LOGIN', + 'mail_smtpname' => getenv('SMTP_NAME') ?: '', + 'mail_from_address' => getenv('MAIL_FROM_ADDRESS'), + 'mail_domain' => getenv('MAIL_DOMAIN'), + ); + + if (getenv('SMTP_PASSWORD_FILE')) { + $CONFIG['mail_smtppassword'] = trim(file_get_contents(getenv('SMTP_PASSWORD_FILE'))); + } elseif (getenv('SMTP_PASSWORD')) { + $CONFIG['mail_smtppassword'] = getenv('SMTP_PASSWORD'); + } else { + $CONFIG['mail_smtppassword'] = ''; + } +} diff --git a/docker/configs/nextcloud/swift.config.php b/docker/configs/nextcloud/swift.config.php new file mode 100644 index 00000000..47ada566 --- /dev/null +++ b/docker/configs/nextcloud/swift.config.php @@ -0,0 +1,31 @@ + [ + 'class' => 'OC\\Files\\ObjectStore\\Swift', + 'arguments' => [ + 'autocreate' => $autocreate == true && strtolower($autocreate) !== 'false', + 'user' => [ + 'name' => getenv('OBJECTSTORE_SWIFT_USER_NAME'), + 'password' => getenv('OBJECTSTORE_SWIFT_USER_PASSWORD'), + 'domain' => [ + 'name' => (getenv('OBJECTSTORE_SWIFT_USER_DOMAIN')) ?: 'Default', + ], + ], + 'scope' => [ + 'project' => [ + 'name' => getenv('OBJECTSTORE_SWIFT_PROJECT_NAME'), + 'domain' => [ + 'name' => (getenv('OBJECTSTORE_SWIFT_PROJECT_DOMAIN')) ?: 'Default', + ], + ], + ], + 'serviceName' => (getenv('OBJECTSTORE_SWIFT_SERVICE_NAME')) ?: 'swift', + 'region' => getenv('OBJECTSTORE_SWIFT_REGION'), + 'url' => getenv('OBJECTSTORE_SWIFT_URL'), + 'bucket' => getenv('OBJECTSTORE_SWIFT_CONTAINER_NAME'), + ] + ] + ); +} diff --git a/docker/configs/nextcloud/upgrade-disable-web.config.php b/docker/configs/nextcloud/upgrade-disable-web.config.php new file mode 100644 index 00000000..cb00b436 --- /dev/null +++ b/docker/configs/nextcloud/upgrade-disable-web.config.php @@ -0,0 +1,4 @@ + true, +); diff --git a/docker/configs/owncloud/apache-pretty-urls.config.php b/docker/configs/owncloud/apache-pretty-urls.config.php new file mode 100644 index 00000000..72da1d8c --- /dev/null +++ b/docker/configs/owncloud/apache-pretty-urls.config.php @@ -0,0 +1,4 @@ + '/', +); diff --git a/docker/configs/owncloud/apache.conf b/docker/configs/owncloud/apache.conf new file mode 100644 index 00000000..a106f0ff --- /dev/null +++ b/docker/configs/owncloud/apache.conf @@ -0,0 +1,41 @@ + + DocumentRoot /var/www/html + ServerName ${OWNCLOUD_HOST} + Redirect permanent / https://${OWNCLOUD_HOST}/ + + LogLevel warn + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + + + DocumentRoot /var/www/html + ServerName ${OWNCLOUD_HOST} + + Protocols h2 http/1.1 + + LogLevel ${OWNCLOUD_APACHE_LOGLEVEL} + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + Header always set Strict-Transport-Security "max-age=63072000;" + + SSLEngine on + SSLCertificateFile "/tls/server.crt" + SSLCertificateKeyFile "/tls/server.key" + + + Require all granted + Options FollowSymlinks MultiViews + AllowOverride All + + + Dav off + + + SetEnv HOME /var/www/html/ + SetEnv HTTP_HOME /var/www/html/ + Satisfy Any + + diff --git a/docker/configs/owncloud/apcu.config.php b/docker/configs/owncloud/apcu.config.php new file mode 100644 index 00000000..69fed876 --- /dev/null +++ b/docker/configs/owncloud/apcu.config.php @@ -0,0 +1,4 @@ + '\OC\Memcache\APCu', +); diff --git a/docker/configs/owncloud/apps.config.php b/docker/configs/owncloud/apps.config.php new file mode 100644 index 00000000..4c37f72a --- /dev/null +++ b/docker/configs/owncloud/apps.config.php @@ -0,0 +1,15 @@ + array ( + 0 => array ( + 'path' => OC::$SERVERROOT.'/apps', + 'url' => '/apps', + 'writable' => false, + ), + 1 => array ( + 'path' => OC::$SERVERROOT.'/custom_apps', + 'url' => '/custom_apps', + 'writable' => true, + ), + ), +); diff --git a/docker/configs/owncloud/autoconfig.php b/docker/configs/owncloud/autoconfig.php new file mode 100644 index 00000000..92ad2a1c --- /dev/null +++ b/docker/configs/owncloud/autoconfig.php @@ -0,0 +1,41 @@ + '\OC\Memcache\Redis', + 'memcache.locking' => '\OC\Memcache\Redis', + 'redis' => array( + 'host' => getenv('REDIS_HOST'), + 'password' => getenv('REDIS_HOST_PASSWORD_FILE') ? trim(file_get_contents(getenv('REDIS_HOST_PASSWORD_FILE'))) : (string) getenv('REDIS_HOST_PASSWORD'), + ), + ); + + if (getenv('REDIS_HOST_PORT') !== false) { + $CONFIG['redis']['port'] = (int) getenv('REDIS_HOST_PORT'); + } elseif (getenv('REDIS_HOST')[0] != '/') { + $CONFIG['redis']['port'] = 6379; + } +} diff --git a/docker/configs/owncloud/reverse-proxy.config.php b/docker/configs/owncloud/reverse-proxy.config.php new file mode 100644 index 00000000..30c660ff --- /dev/null +++ b/docker/configs/owncloud/reverse-proxy.config.php @@ -0,0 +1,35 @@ + array( + 'class' => '\OC\Files\ObjectStore\S3', + 'arguments' => array( + 'bucket' => getenv('OBJECTSTORE_S3_BUCKET'), + 'region' => getenv('OBJECTSTORE_S3_REGION') ?: '', + 'hostname' => getenv('OBJECTSTORE_S3_HOST') ?: '', + 'port' => getenv('OBJECTSTORE_S3_PORT') ?: '', + 'storageClass' => getenv('OBJECTSTORE_S3_STORAGE_CLASS') ?: '', + 'objectPrefix' => getenv("OBJECTSTORE_S3_OBJECT_PREFIX") ? getenv("OBJECTSTORE_S3_OBJECT_PREFIX") : "urn:oid:", + 'autocreate' => strtolower($autocreate) !== 'false', + 'use_ssl' => strtolower($use_ssl) !== 'false', + // required for some non Amazon S3 implementations + 'use_path_style' => $use_path == true && strtolower($use_path) !== 'false', + // required for older protocol versions + 'legacy_auth' => $use_legacyauth == true && strtolower($use_legacyauth) !== 'false' + ) + ) + ); + + if (getenv('OBJECTSTORE_S3_KEY_FILE')) { + $CONFIG['objectstore']['arguments']['key'] = trim(file_get_contents(getenv('OBJECTSTORE_S3_KEY_FILE'))); + } elseif (getenv('OBJECTSTORE_S3_KEY')) { + $CONFIG['objectstore']['arguments']['key'] = getenv('OBJECTSTORE_S3_KEY'); + } else { + $CONFIG['objectstore']['arguments']['key'] = ''; + } + + if (getenv('OBJECTSTORE_S3_SECRET_FILE')) { + $CONFIG['objectstore']['arguments']['secret'] = trim(file_get_contents(getenv('OBJECTSTORE_S3_SECRET_FILE'))); + } elseif (getenv('OBJECTSTORE_S3_SECRET')) { + $CONFIG['objectstore']['arguments']['secret'] = getenv('OBJECTSTORE_S3_SECRET'); + } else { + $CONFIG['objectstore']['arguments']['secret'] = ''; + } + + if (getenv('OBJECTSTORE_S3_SSE_C_KEY_FILE')) { + $CONFIG['objectstore']['arguments']['sse_c_key'] = trim(file_get_contents(getenv('OBJECTSTORE_S3_SSE_C_KEY_FILE'))); + } elseif (getenv('OBJECTSTORE_S3_SSE_C_KEY')) { + $CONFIG['objectstore']['arguments']['sse_c_key'] = getenv('OBJECTSTORE_S3_SSE_C_KEY'); + } +} diff --git a/docker/configs/owncloud/smtp.config.php b/docker/configs/owncloud/smtp.config.php new file mode 100644 index 00000000..66a2ef7e --- /dev/null +++ b/docker/configs/owncloud/smtp.config.php @@ -0,0 +1,22 @@ + 'smtp', + 'mail_smtphost' => getenv('SMTP_HOST'), + 'mail_smtpport' => getenv('SMTP_PORT') ?: (getenv('SMTP_SECURE') ? 465 : 25), + 'mail_smtpsecure' => getenv('SMTP_SECURE') ?: '', + 'mail_smtpauth' => getenv('SMTP_NAME') && (getenv('SMTP_PASSWORD') || getenv('SMTP_PASSWORD_FILE')), + 'mail_smtpauthtype' => getenv('SMTP_AUTHTYPE') ?: 'LOGIN', + 'mail_smtpname' => getenv('SMTP_NAME') ?: '', + 'mail_from_address' => getenv('MAIL_FROM_ADDRESS'), + 'mail_domain' => getenv('MAIL_DOMAIN'), + ); + + if (getenv('SMTP_PASSWORD_FILE')) { + $CONFIG['mail_smtppassword'] = trim(file_get_contents(getenv('SMTP_PASSWORD_FILE'))); + } elseif (getenv('SMTP_PASSWORD')) { + $CONFIG['mail_smtppassword'] = getenv('SMTP_PASSWORD'); + } else { + $CONFIG['mail_smtppassword'] = ''; + } +} diff --git a/docker/configs/owncloud/swift.config.php b/docker/configs/owncloud/swift.config.php new file mode 100644 index 00000000..47ada566 --- /dev/null +++ b/docker/configs/owncloud/swift.config.php @@ -0,0 +1,31 @@ + [ + 'class' => 'OC\\Files\\ObjectStore\\Swift', + 'arguments' => [ + 'autocreate' => $autocreate == true && strtolower($autocreate) !== 'false', + 'user' => [ + 'name' => getenv('OBJECTSTORE_SWIFT_USER_NAME'), + 'password' => getenv('OBJECTSTORE_SWIFT_USER_PASSWORD'), + 'domain' => [ + 'name' => (getenv('OBJECTSTORE_SWIFT_USER_DOMAIN')) ?: 'Default', + ], + ], + 'scope' => [ + 'project' => [ + 'name' => getenv('OBJECTSTORE_SWIFT_PROJECT_NAME'), + 'domain' => [ + 'name' => (getenv('OBJECTSTORE_SWIFT_PROJECT_DOMAIN')) ?: 'Default', + ], + ], + ], + 'serviceName' => (getenv('OBJECTSTORE_SWIFT_SERVICE_NAME')) ?: 'swift', + 'region' => getenv('OBJECTSTORE_SWIFT_REGION'), + 'url' => getenv('OBJECTSTORE_SWIFT_URL'), + 'bucket' => getenv('OBJECTSTORE_SWIFT_CONTAINER_NAME'), + ] + ] + ); +} diff --git a/docker/configs/owncloud/upgrade-disable-web.config.php b/docker/configs/owncloud/upgrade-disable-web.config.php new file mode 100644 index 00000000..cb00b436 --- /dev/null +++ b/docker/configs/owncloud/upgrade-disable-web.config.php @@ -0,0 +1,4 @@ + true, +); diff --git a/docker/scripts/nextcloud/cron.sh b/docker/scripts/nextcloud/cron.sh new file mode 100755 index 00000000..b4cd9af6 --- /dev/null +++ b/docker/scripts/nextcloud/cron.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +exec busybox crond -f -L /dev/stdout diff --git a/docker/scripts/nextcloud/entrypoint.sh b/docker/scripts/nextcloud/entrypoint.sh new file mode 100755 index 00000000..06ca638b --- /dev/null +++ b/docker/scripts/nextcloud/entrypoint.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Docker Initialization Script for TLS Certificates +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script runs inside a Docker container to set up TLS certificates for a given +# host. It copies certificates and keys from a specified directory, validates their +# presence, and creates symbolic links for easy reference by the main service. +# Finally, it executes the provided command (e.g., the container's CMD). +# +# Requirements: +# - The HOST environment variable must be set. +# - The /certificates directory should contain .crt and .key files matching the HOST. +# +# Usage: +# In a Dockerfile: +# COPY entrypoint.sh /entrypoint.sh +# ENTRYPOINT ["/entrypoint.sh"] +# +# The container's CMD will be executed by this script once TLS setup is complete. +# +# Notes: +# - If HOST is "example.com", then the script expects to find "example.com.crt" and +# "example.com.key" inside the /tls directory after copying from /certificates. +# - The script creates /tls directory if it doesn't exist. +# - Symbolic links are created: +# /tls/server.crt -> /tls/${HOST}.crt +# /tls/server.key -> /tls/${HOST}.key +# +# Example: +# HOST=example.com docker run --rm -e HOST=example.com myimage:latest +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to missing HOST, missing certificates, or command execution issues. + +# ----------------------------------------------------------------------------------- +# Safety and Error Handling +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# Define a trap to clean up resources on script exit or error. +# This ensures the /tls directory is removed if something goes wrong. +trap 'rm -rf /tls' EXIT + +# ----------------------------------------------------------------------------------- +# Directory and Certificate Setup +# ----------------------------------------------------------------------------------- + +# Ensure the /tls directory exists +mkdir -p /tls + +# If the /certificates directory exists, copy .crt and .key files to /tls +if [[ -d "/certificates" ]]; then + printf "Copying certificates and keys to /tls...\n" + # Copy .crt files + find /certificates -type f -name "*.crt" -exec cp -f {} /tls/ \; + + # Copy .key files + find /certificates -type f -name "*.key" -exec cp -f {} /tls/ \; + + printf "Certificate and key files copied successfully.\n" +else + printf "Warning: /certificates directory does not exist. Skipping certificate copy.\n" >&2 +fi + +# ----------------------------------------------------------------------------------- +# Validate HOST Environment Variable +# ----------------------------------------------------------------------------------- + +# Ensure the HOST environment variable is set +if [[ -z "${HOST}" ]]; then + printf "Error: HOST environment variable is not set. Aborting.\n" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Check for HOST-Specific Certificate and Key +# ----------------------------------------------------------------------------------- +crt_path="/tls/${HOST}.crt" +key_path="/tls/${HOST}.key" + +if [[ -f "${crt_path}" && -f "${key_path}" ]]; then + printf "Creating symbolic links for certificates...\n" + # Create symbolic links to /tls/server.crt and /tls/server.key + ln --symbolic --force "${crt_path}" /tls/server.crt + ln --symbolic --force "${key_path}" /tls/server.key + + printf "Symbolic links created successfully.\n" +else + printf "Error: Certificate or key file for host '%s' not found in /tls.\n" "${HOST}" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Execute the Provided Command +# ----------------------------------------------------------------------------------- + +# Print the command that is about to be executed +printf "Executing command: /init.sh\n" + +# Validate that /init.sh exists and is executable +if [[ ! -x "/init.sh" ]]; then + printf "Error: /init.sh does not exist or is not executable. Aborting.\n" >&2 + exit 1 +fi + +# Execute init +"/init.sh" "${3}" + +# Print the command that is about to be executed +printf "Executing command: %s\n" "$*" + +# Execute the command using 'exec' so that it becomes the main process in the container +exec "$@" diff --git a/docker/scripts/nextcloud/init.sh b/docker/scripts/nextcloud/init.sh new file mode 100755 index 00000000..d18e2549 --- /dev/null +++ b/docker/scripts/nextcloud/init.sh @@ -0,0 +1,302 @@ +#!/bin/sh +set -eu + +# version_greater A B returns whether A > B +version_greater() { + [ "$(printf '%s\n' "$@" | sort -t '.' -n -k1,1 -k2,2 -k3,3 -k4,4 | head -n 1)" != "$1" ] +} + +# return true if specified directory is empty +directory_empty() { + [ -z "$(ls -A "$1/")" ] +} + +run_as() { + if [ "$(id -u)" = 0 ]; then + su -p "$user" -s /bin/sh -c "$1" + else + sh -c "$1" + fi +} + +# Execute all executable files in a given directory in alphanumeric order +run_path() { + local hook_folder_path="/docker-entrypoint-hooks.d/$1" + local return_code=0 + + if ! [ -d "${hook_folder_path}" ]; then + echo "=> Skipping the folder \"${hook_folder_path}\", because it doesn't exist" + return 0 + fi + + echo "=> Searching for scripts (*.sh) to run, located in the folder: ${hook_folder_path}" + + ( + find "${hook_folder_path}" -maxdepth 1 -iname '*.sh' '(' -type f -o -type l ')' -print | sort | while read -r script_file_path; do + if ! [ -x "${script_file_path}" ]; then + echo "==> The script \"${script_file_path}\" was skipped, because it didn't have the executable flag" + continue + fi + + echo "==> Running the script (cwd: $(pwd)): \"${script_file_path}\"" + + run_as "${script_file_path}" || return_code="$?" + + if [ "${return_code}" -ne "0" ]; then + echo "==> Failed at executing \"${script_file_path}\". Exit code: ${return_code}" + exit 1 + fi + + echo "==> Finished the script: \"${script_file_path}\"" + done + ) +} + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + local varValue=$(env | grep -E "^${var}=" | sed -E -e "s/^${var}=//") + local fileVarValue=$(env | grep -E "^${fileVar}=" | sed -E -e "s/^${fileVar}=//") + if [ -n "${varValue}" ] && [ -n "${fileVarValue}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + if [ -n "${varValue}" ]; then + export "$var"="${varValue}" + elif [ -n "${fileVarValue}" ]; then + export "$var"="$(cat "${fileVarValue}")" + elif [ -n "${def}" ]; then + export "$var"="$def" + fi + unset "$fileVar" +} + +if expr "$1" : "apache" 1>/dev/null; then + if [ -n "${APACHE_DISABLE_REWRITE_IP+x}" ]; then + a2disconf remoteip + fi +fi + +if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ] || [ "${NEXTCLOUD_UPDATE:-0}" -eq 1 ]; then + uid="$(id -u)" + gid="$(id -g)" + if [ "$uid" = '0' ]; then + case "$1" in + apache2*) + user="${APACHE_RUN_USER:-www-data}" + group="${APACHE_RUN_GROUP:-www-data}" + + # strip off any '#' symbol ('#1000' is valid syntax for Apache) + user="${user#'#'}" + group="${group#'#'}" + ;; + *) # php-fpm + user='www-data' + group='www-data' + ;; + esac + else + user="$uid" + group="$gid" + fi + + if [ -n "${REDIS_HOST+x}" ]; then + + echo "Configuring Redis as session handler" + { + file_env REDIS_HOST_PASSWORD + echo 'session.save_handler = redis' + # check if redis host is an unix socket path + if [ "$(echo "$REDIS_HOST" | cut -c1-1)" = "/" ]; then + if [ -n "${REDIS_HOST_PASSWORD+x}" ]; then + echo "session.save_path = \"unix://${REDIS_HOST}?auth=${REDIS_HOST_PASSWORD}\"" + else + echo "session.save_path = \"unix://${REDIS_HOST}\"" + fi + # check if redis password has been set + elif [ -n "${REDIS_HOST_PASSWORD+x}" ]; then + echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth=${REDIS_HOST_PASSWORD}\"" + else + echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}\"" + fi + echo "redis.session.locking_enabled = 1" + echo "redis.session.lock_retries = -1" + # redis.session.lock_wait_time is specified in microseconds. + # Wait 10ms before retrying the lock rather than the default 2ms. + echo "redis.session.lock_wait_time = 10000" + } >/usr/local/etc/php/conf.d/redis-session.ini + fi + + # If another process is syncing the html folder, wait for + # it to be done, then escape initalization. + ( + if ! flock -n 9; then + # If we couldn't get it immediately, show a message, then wait for real + echo "Another process is initializing Nextcloud. Waiting..." + flock 9 + fi + + installed_version="0.0.0.0" + if [ -f /var/www/html/version.php ]; then + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" + fi + # shellcheck disable=SC2016 + image_version="$(php -r 'require "/usr/src/nextcloud/version.php"; echo implode(".", $OC_Version);')" + + # if version_greater "$installed_version" "$image_version"; then + # echo "Can't start Nextcloud because the version of the data ($installed_version) is higher than the docker image version ($image_version) and downgrading is not supported. Are you sure you have pulled the newest image version?" + # exit 1 + # fi + + if version_greater "$image_version" "$installed_version"; then + echo "Initializing nextcloud $image_version ..." + if [ "$installed_version" != "0.0.0.0" ]; then + if [ "${image_version%%.*}" -gt "$((${installed_version%%.*} + 1))" ]; then + echo "Can't start Nextcloud because upgrading from $installed_version to $image_version is not supported." + echo "It is only possible to upgrade one major version at a time. For example, if you want to upgrade from version 14 to 16, you will have to upgrade from version 14 to 15, then from 15 to 16." + exit 1 + fi + echo "Upgrading nextcloud from $installed_version ..." + run_as 'php /var/www/html/occ app:list' | sed -n "/Enabled:/,/Disabled:/p" >/tmp/list_before + fi + if [ "$(id -u)" = 0 ]; then + rsync_options="-rlDog --chown $user:$group" + else + rsync_options="-rlD" + fi + + rsync $rsync_options --delete --exclude-from=/upgrade.exclude /usr/src/nextcloud/ /var/www/html/ + for dir in config data custom_apps themes; do + if [ ! -d "/var/www/html/$dir" ] || directory_empty "/var/www/html/$dir"; then + rsync $rsync_options --include "/$dir/" --exclude '/*' /usr/src/nextcloud/ /var/www/html/ + fi + done + rsync $rsync_options --include '/version.php' --exclude '/*' /usr/src/nextcloud/ /var/www/html/ + + # Install + if [ "$installed_version" = "0.0.0.0" ]; then + echo "New nextcloud instance" + + file_env NEXTCLOUD_ADMIN_PASSWORD + file_env NEXTCLOUD_ADMIN_USER + + install=false + if [ -n "${NEXTCLOUD_ADMIN_USER+x}" ] && [ -n "${NEXTCLOUD_ADMIN_PASSWORD+x}" ]; then + # shellcheck disable=SC2016 + install_options='-n --admin-user "$NEXTCLOUD_ADMIN_USER" --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD"' + if [ -n "${NEXTCLOUD_DATA_DIR+x}" ]; then + # shellcheck disable=SC2016 + install_options=$install_options' --data-dir "$NEXTCLOUD_DATA_DIR"' + fi + + file_env MYSQL_DATABASE + file_env MYSQL_PASSWORD + file_env MYSQL_USER + file_env POSTGRES_DB + file_env POSTGRES_PASSWORD + file_env POSTGRES_USER + + if [ -n "${SQLITE_DATABASE+x}" ]; then + echo "Installing with SQLite database" + # shellcheck disable=SC2016 + install_options=$install_options' --database-name "$SQLITE_DATABASE"' + install=true + elif [ -n "${MYSQL_DATABASE+x}" ] && [ -n "${MYSQL_USER+x}" ] && [ -n "${MYSQL_PASSWORD+x}" ] && [ -n "${MYSQL_HOST+x}" ]; then + echo "Installing with MySQL database" + # shellcheck disable=SC2016 + install_options=$install_options' --database mysql --database-name "$MYSQL_DATABASE" --database-user "$MYSQL_USER" --database-pass "$MYSQL_PASSWORD" --database-host "$MYSQL_HOST"' + install=true + elif [ -n "${POSTGRES_DB+x}" ] && [ -n "${POSTGRES_USER+x}" ] && [ -n "${POSTGRES_PASSWORD+x}" ] && [ -n "${POSTGRES_HOST+x}" ]; then + echo "Installing with PostgreSQL database" + # shellcheck disable=SC2016 + install_options=$install_options' --database pgsql --database-name "$POSTGRES_DB" --database-user "$POSTGRES_USER" --database-pass "$POSTGRES_PASSWORD" --database-host "$POSTGRES_HOST"' + install=true + fi + + if [ "$install" = true ]; then + run_path pre-installation + + echo "Starting nextcloud installation" + max_retries=10 + try=0 + until [ "$try" -gt "$max_retries" ] || run_as "php /var/www/html/occ maintenance:install $install_options"; do + echo "Retrying install..." + try=$((try + 1)) + sleep 10s + done + if [ "$try" -gt "$max_retries" ]; then + echo "Installing of nextcloud failed!" + exit 1 + fi + if [ -n "${NEXTCLOUD_TRUSTED_DOMAINS+x}" ]; then + echo "Setting trusted domains…" + NC_TRUSTED_DOMAIN_IDX=1 + for DOMAIN in $NEXTCLOUD_TRUSTED_DOMAINS; do + DOMAIN=$(echo "$DOMAIN" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + run_as "php /var/www/html/occ config:system:set trusted_domains $NC_TRUSTED_DOMAIN_IDX --value=$DOMAIN" + NC_TRUSTED_DOMAIN_IDX=$((NC_TRUSTED_DOMAIN_IDX + 1)) + done + fi + + # additional configs. + run_as "php /var/www/html/occ db:add-missing-indices" + run_as "php /var/www/html/occ maintenance:repair --include-expensive" + run_as "php /var/www/html/occ config:system:set maintenance_window_start --type=integer --value=1" + + sed -i "3 i\ 'allow_local_remote_servers' => true," /var/www/html/config/config.php + + # disable first run wizard + run_as "php /var/www/html/console.php app:disable firstrunwizard" + # create the log file + run_as "touch /var/www/html/data/nextcloud.log" + + run_path post-installation + fi + fi + # not enough specified to do a fully automated installation + if [ "$install" = false ]; then + echo "Next step: Access your instance to finish the web-based installation!" + echo "Hint: You can specify NEXTCLOUD_ADMIN_USER and NEXTCLOUD_ADMIN_PASSWORD and the database variables _prior to first launch_ to fully automate initial installation." + fi + # Upgrade + else + run_path pre-upgrade + + run_as 'php /var/www/html/occ upgrade' + + run_as 'php /var/www/html/occ app:list' | sed -n "/Enabled:/,/Disabled:/p" >/tmp/list_after + echo "The following apps have been disabled:" + diff /tmp/list_before /tmp/list_after | grep '<' | cut -d- -f2 | cut -d: -f1 + rm -f /tmp/list_before /tmp/list_after + + run_path post-upgrade + fi + + echo "Initializing finished" + fi + + # Update htaccess after init if requested + if [ -n "${NEXTCLOUD_INIT_HTACCESS+x}" ] && [ "$installed_version" != "0.0.0.0" ]; then + run_as 'php /var/www/html/occ maintenance:update:htaccess' + fi + ) 9>/var/www/html/nextcloud-init-sync.lock + + # warn if config files on persistent storage differ from the latest version of this image + for cfgPath in /usr/src/nextcloud/config/*.php; do + cfgFile=$(basename "$cfgPath") + + if [ "$cfgFile" != "config.sample.php" ] && [ "$cfgFile" != "autoconfig.php" ]; then + if ! cmp -s "/usr/src/nextcloud/config/$cfgFile" "/var/www/html/config/$cfgFile"; then + echo "Warning: /var/www/html/config/$cfgFile differs from the latest version of this image at /usr/src/nextcloud/config/$cfgFile" + fi + fi + done + + run_path before-starting +fi diff --git a/docker/scripts/nextcloud/upgrade.exclude b/docker/scripts/nextcloud/upgrade.exclude new file mode 100644 index 00000000..31ce39a8 --- /dev/null +++ b/docker/scripts/nextcloud/upgrade.exclude @@ -0,0 +1,6 @@ +/config/ +/data/ +/custom_apps/ +/themes/ +/version.php +/nextcloud-init-sync.lock diff --git a/docker/scripts/owncloud/entrypoint.sh b/docker/scripts/owncloud/entrypoint.sh new file mode 100755 index 00000000..06ca638b --- /dev/null +++ b/docker/scripts/owncloud/entrypoint.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Docker Initialization Script for TLS Certificates +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script runs inside a Docker container to set up TLS certificates for a given +# host. It copies certificates and keys from a specified directory, validates their +# presence, and creates symbolic links for easy reference by the main service. +# Finally, it executes the provided command (e.g., the container's CMD). +# +# Requirements: +# - The HOST environment variable must be set. +# - The /certificates directory should contain .crt and .key files matching the HOST. +# +# Usage: +# In a Dockerfile: +# COPY entrypoint.sh /entrypoint.sh +# ENTRYPOINT ["/entrypoint.sh"] +# +# The container's CMD will be executed by this script once TLS setup is complete. +# +# Notes: +# - If HOST is "example.com", then the script expects to find "example.com.crt" and +# "example.com.key" inside the /tls directory after copying from /certificates. +# - The script creates /tls directory if it doesn't exist. +# - Symbolic links are created: +# /tls/server.crt -> /tls/${HOST}.crt +# /tls/server.key -> /tls/${HOST}.key +# +# Example: +# HOST=example.com docker run --rm -e HOST=example.com myimage:latest +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to missing HOST, missing certificates, or command execution issues. + +# ----------------------------------------------------------------------------------- +# Safety and Error Handling +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# Define a trap to clean up resources on script exit or error. +# This ensures the /tls directory is removed if something goes wrong. +trap 'rm -rf /tls' EXIT + +# ----------------------------------------------------------------------------------- +# Directory and Certificate Setup +# ----------------------------------------------------------------------------------- + +# Ensure the /tls directory exists +mkdir -p /tls + +# If the /certificates directory exists, copy .crt and .key files to /tls +if [[ -d "/certificates" ]]; then + printf "Copying certificates and keys to /tls...\n" + # Copy .crt files + find /certificates -type f -name "*.crt" -exec cp -f {} /tls/ \; + + # Copy .key files + find /certificates -type f -name "*.key" -exec cp -f {} /tls/ \; + + printf "Certificate and key files copied successfully.\n" +else + printf "Warning: /certificates directory does not exist. Skipping certificate copy.\n" >&2 +fi + +# ----------------------------------------------------------------------------------- +# Validate HOST Environment Variable +# ----------------------------------------------------------------------------------- + +# Ensure the HOST environment variable is set +if [[ -z "${HOST}" ]]; then + printf "Error: HOST environment variable is not set. Aborting.\n" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Check for HOST-Specific Certificate and Key +# ----------------------------------------------------------------------------------- +crt_path="/tls/${HOST}.crt" +key_path="/tls/${HOST}.key" + +if [[ -f "${crt_path}" && -f "${key_path}" ]]; then + printf "Creating symbolic links for certificates...\n" + # Create symbolic links to /tls/server.crt and /tls/server.key + ln --symbolic --force "${crt_path}" /tls/server.crt + ln --symbolic --force "${key_path}" /tls/server.key + + printf "Symbolic links created successfully.\n" +else + printf "Error: Certificate or key file for host '%s' not found in /tls.\n" "${HOST}" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Execute the Provided Command +# ----------------------------------------------------------------------------------- + +# Print the command that is about to be executed +printf "Executing command: /init.sh\n" + +# Validate that /init.sh exists and is executable +if [[ ! -x "/init.sh" ]]; then + printf "Error: /init.sh does not exist or is not executable. Aborting.\n" >&2 + exit 1 +fi + +# Execute init +"/init.sh" "${3}" + +# Print the command that is about to be executed +printf "Executing command: %s\n" "$*" + +# Execute the command using 'exec' so that it becomes the main process in the container +exec "$@" diff --git a/docker/scripts/owncloud/init.sh b/docker/scripts/owncloud/init.sh new file mode 100755 index 00000000..5e768ba5 --- /dev/null +++ b/docker/scripts/owncloud/init.sh @@ -0,0 +1,303 @@ +#!/bin/sh +set -eu + +# version_greater A B returns whether A > B +version_greater() { + [ "$(printf '%s\n' "$@" | sort -t '.' -n -k1,1 -k2,2 -k3,3 -k4,4 | head -n 1)" != "$1" ] +} + +# return true if specified directory is empty +directory_empty() { + [ -z "$(ls -A "$1/")" ] +} + +run_as() { + if [ "$(id -u)" = 0 ]; then + su -p "$user" -s /bin/sh -c "$1" + else + sh -c "$1" + fi +} + +# Execute all executable files in a given directory in alphanumeric order +run_path() { + local hook_folder_path="/docker-entrypoint-hooks.d/$1" + local return_code=0 + + if ! [ -d "${hook_folder_path}" ]; then + echo "=> Skipping the folder \"${hook_folder_path}\", because it doesn't exist" + return 0 + fi + + echo "=> Searching for scripts (*.sh) to run, located in the folder: ${hook_folder_path}" + + ( + find "${hook_folder_path}" -maxdepth 1 -iname '*.sh' '(' -type f -o -type l ')' -print | sort | while read -r script_file_path; do + if ! [ -x "${script_file_path}" ]; then + echo "==> The script \"${script_file_path}\" was skipped, because it didn't have the executable flag" + continue + fi + + echo "==> Running the script (cwd: $(pwd)): \"${script_file_path}\"" + + run_as "${script_file_path}" || return_code="$?" + + if [ "${return_code}" -ne "0" ]; then + echo "==> Failed at executing \"${script_file_path}\". Exit code: ${return_code}" + exit 1 + fi + + echo "==> Finished the script: \"${script_file_path}\"" + done + ) +} + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + local varValue=$(env | grep -E "^${var}=" | sed -E -e "s/^${var}=//") + local fileVarValue=$(env | grep -E "^${fileVar}=" | sed -E -e "s/^${fileVar}=//") + if [ -n "${varValue}" ] && [ -n "${fileVarValue}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + if [ -n "${varValue}" ]; then + export "$var"="${varValue}" + elif [ -n "${fileVarValue}" ]; then + export "$var"="$(cat "${fileVarValue}")" + elif [ -n "${def}" ]; then + export "$var"="$def" + fi + unset "$fileVar" +} + +if expr "$1" : "apache" 1>/dev/null; then + if [ -n "${APACHE_DISABLE_REWRITE_IP+x}" ]; then + a2disconf remoteip + fi +fi + +if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ] || [ "${OWNCLOUD_UPDATE:-0}" -eq 1 ]; then + uid="$(id -u)" + gid="$(id -g)" + if [ "$uid" = '0' ]; then + case "$1" in + apache2*) + user="${APACHE_RUN_USER:-www-data}" + group="${APACHE_RUN_GROUP:-www-data}" + + # strip off any '#' symbol ('#1000' is valid syntax for Apache) + user="${user#'#'}" + group="${group#'#'}" + ;; + *) # php-fpm + user='www-data' + group='www-data' + ;; + esac + else + user="$uid" + group="$gid" + fi + + if [ -n "${REDIS_HOST+x}" ]; then + + echo "Configuring Redis as session handler" + { + file_env REDIS_HOST_PASSWORD + echo 'session.save_handler = redis' + # check if redis host is an unix socket path + if [ "$(echo "$REDIS_HOST" | cut -c1-1)" = "/" ]; then + if [ -n "${REDIS_HOST_PASSWORD+x}" ]; then + echo "session.save_path = \"unix://${REDIS_HOST}?auth=${REDIS_HOST_PASSWORD}\"" + else + echo "session.save_path = \"unix://${REDIS_HOST}\"" + fi + # check if redis password has been set + elif [ -n "${REDIS_HOST_PASSWORD+x}" ]; then + echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth=${REDIS_HOST_PASSWORD}\"" + else + echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}\"" + fi + echo "redis.session.locking_enabled = 1" + echo "redis.session.lock_retries = -1" + # redis.session.lock_wait_time is specified in microseconds. + # Wait 10ms before retrying the lock rather than the default 2ms. + echo "redis.session.lock_wait_time = 10000" + } >/usr/local/etc/php/conf.d/redis-session.ini + fi + + # If another process is syncing the html folder, wait for + # it to be done, then escape initalization. + ( + if ! flock -n 9; then + # If we couldn't get it immediately, show a message, then wait for real + echo "Another process is initializing ownCloud. Waiting..." + flock 9 + fi + + installed_version="0.0.0.0" + if [ -f /var/www/html/version.php ]; then + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" + fi + # shellcheck disable=SC2016 + image_version="$(php -r 'require "/usr/src/owncloud/version.php"; echo implode(".", $OC_Version);')" + + # if version_greater "$installed_version" "$image_version"; then + # echo "Can't start ownCloud because the version of the data ($installed_version) is higher than the docker image version ($image_version) and downgrading is not supported. Are you sure you have pulled the newest image version?" + # exit 1 + # fi + + if version_greater "$image_version" "$installed_version"; then + echo "Initializing owncloud $image_version ..." + if [ "$installed_version" != "0.0.0.0" ]; then + if [ "${image_version%%.*}" -gt "$((${installed_version%%.*} + 1))" ]; then + echo "Can't start ownCloud because upgrading from $installed_version to $image_version is not supported." + echo "It is only possible to upgrade one major version at a time. For example, if you want to upgrade from version 14 to 16, you will have to upgrade from version 14 to 15, then from 15 to 16." + exit 1 + fi + echo "Upgrading owncloud from $installed_version ..." + run_as 'php /var/www/html/occ app:list' | sed -n "/Enabled:/,/Disabled:/p" >/tmp/list_before + fi + if [ "$(id -u)" = 0 ]; then + rsync_options="-rlDog --chown $user:$group" + else + rsync_options="-rlD" + fi + + rsync $rsync_options --delete --exclude-from=/upgrade.exclude /usr/src/owncloud/ /var/www/html/ + for dir in config data custom_apps themes; do + if [ ! -d "/var/www/html/$dir" ] || directory_empty "/var/www/html/$dir"; then + rsync $rsync_options --include "/$dir/" --exclude '/*' /usr/src/owncloud/ /var/www/html/ + fi + done + rsync $rsync_options --include '/version.php' --exclude '/*' /usr/src/owncloud/ /var/www/html/ + + # Install + if [ "$installed_version" = "0.0.0.0" ]; then + echo "New owncloud instance" + + file_env OWNCLOUD_ADMIN_PASSWORD + file_env OWNCLOUD_ADMIN_USER + + install=false + if [ -n "${OWNCLOUD_ADMIN_USER+x}" ] && [ -n "${OWNCLOUD_ADMIN_PASSWORD+x}" ]; then + # shellcheck disable=SC2016 + install_options='-n --admin-user "$OWNCLOUD_ADMIN_USER" --admin-pass "$OWNCLOUD_ADMIN_PASSWORD"' + if [ -n "${OWNCLOUD_DATA_DIR+x}" ]; then + # shellcheck disable=SC2016 + install_options=$install_options' --data-dir "$OWNCLOUD_DATA_DIR"' + fi + + file_env MYSQL_DATABASE + file_env MYSQL_PASSWORD + file_env MYSQL_USER + file_env POSTGRES_DB + file_env POSTGRES_PASSWORD + file_env POSTGRES_USER + + if [ -n "${SQLITE_DATABASE+x}" ]; then + echo "Installing with SQLite database" + # shellcheck disable=SC2016 + install_options=$install_options' --database-name "$SQLITE_DATABASE"' + install=true + elif [ -n "${MYSQL_DATABASE+x}" ] && [ -n "${MYSQL_USER+x}" ] && [ -n "${MYSQL_PASSWORD+x}" ] && [ -n "${MYSQL_HOST+x}" ]; then + echo "Installing with MySQL database" + # shellcheck disable=SC2016 + install_options=$install_options' --database mysql --database-name "$MYSQL_DATABASE" --database-user "$MYSQL_USER" --database-pass "$MYSQL_PASSWORD" --database-host "$MYSQL_HOST"' + install=true + elif [ -n "${POSTGRES_DB+x}" ] && [ -n "${POSTGRES_USER+x}" ] && [ -n "${POSTGRES_PASSWORD+x}" ] && [ -n "${POSTGRES_HOST+x}" ]; then + echo "Installing with PostgreSQL database" + # shellcheck disable=SC2016 + install_options=$install_options' --database pgsql --database-name "$POSTGRES_DB" --database-user "$POSTGRES_USER" --database-pass "$POSTGRES_PASSWORD" --database-host "$POSTGRES_HOST"' + install=true + fi + + if [ "$install" = true ]; then + run_path pre-installation + + echo "Starting owncloud installation" + max_retries=10 + try=0 + until [ "$try" -gt "$max_retries" ] || run_as "php /var/www/html/occ maintenance:install $install_options"; do + echo "Retrying install..." + try=$((try + 1)) + sleep 10s + done + if [ "$try" -gt "$max_retries" ]; then + echo "Installing of owncloud failed!" + exit 1 + fi + if [ -n "${OWNCLOUD_TRUSTED_DOMAINS+x}" ]; then + echo "Setting trusted domains…" + NC_TRUSTED_DOMAIN_IDX=1 + for DOMAIN in $OWNCLOUD_TRUSTED_DOMAINS; do + DOMAIN=$(echo "$DOMAIN" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + run_as "php /var/www/html/occ config:system:set trusted_domains $NC_TRUSTED_DOMAIN_IDX --value=$DOMAIN" + NC_TRUSTED_DOMAIN_IDX=$((NC_TRUSTED_DOMAIN_IDX + 1)) + done + fi + + # additional configs. + run_as "php /var/www/html/occ maintenance:repair --include-expensive" + run_as "php /var/www/html/occ config:system:set maintenance_window_start --type=integer --value=1" + + sed -i "3 i\ 'allow_local_remote_servers' => true," /var/www/html/config/config.php + + # disable first run wizard + run_as "php /var/www/html/console.php app:disable firstrunwizard" + # create the log file + run_as "touch /var/www/html/data/owncloud.log" + + run_as "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" + + run_path post-installation + fi + fi + # not enough specified to do a fully automated installation + if [ "$install" = false ]; then + echo "Next step: Access your instance to finish the web-based installation!" + echo "Hint: You can specify OWNCLOUD_ADMIN_USER and OWNCLOUD_ADMIN_PASSWORD and the database variables _prior to first launch_ to fully automate initial installation." + fi + # Upgrade + else + run_path pre-upgrade + + run_as 'php /var/www/html/occ upgrade' + + run_as 'php /var/www/html/occ app:list' | sed -n "/Enabled:/,/Disabled:/p" >/tmp/list_after + echo "The following apps have been disabled:" + diff /tmp/list_before /tmp/list_after | grep '<' | cut -d- -f2 | cut -d: -f1 + rm -f /tmp/list_before /tmp/list_after + + run_path post-upgrade + fi + + echo "Initializing finished" + fi + + # Update htaccess after init if requested + if [ -n "${OWNCLOUD_INIT_HTACCESS+x}" ] && [ "$installed_version" != "0.0.0.0" ]; then + run_as 'php /var/www/html/occ maintenance:update:htaccess' + fi + ) 9>/var/www/html/owncloud-init-sync.lock + + # warn if config files on persistent storage differ from the latest version of this image + for cfgPath in /usr/src/owncloud/config/*.php; do + cfgFile=$(basename "$cfgPath") + + if [ "$cfgFile" != "config.sample.php" ] && [ "$cfgFile" != "autoconfig.php" ]; then + if ! cmp -s "/usr/src/owncloud/config/$cfgFile" "/var/www/html/config/$cfgFile"; then + echo "Warning: /var/www/html/config/$cfgFile differs from the latest version of this image at /usr/src/owncloud/config/$cfgFile" + fi + fi + done + + run_path before-starting +fi From ffad7a2b5676852e8c61e8fb74e1d59df631b259 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 13:41:04 +0000 Subject: [PATCH 034/184] [no ci] add: missing script --- .gitignore | 4 ++-- docker/scripts/owncloud/upgrade.exclude | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 docker/scripts/owncloud/upgrade.exclude diff --git a/.gitignore b/.gitignore index 3cd5f6a3..50422431 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,8 @@ core temp server rd-sram -owncloud -nextcloud +./owncloud +./nextcloud owncloud-sciencemesh nextcloud-sciencemesh open-id-connect diff --git a/docker/scripts/owncloud/upgrade.exclude b/docker/scripts/owncloud/upgrade.exclude new file mode 100644 index 00000000..31ce39a8 --- /dev/null +++ b/docker/scripts/owncloud/upgrade.exclude @@ -0,0 +1,6 @@ +/config/ +/data/ +/custom_apps/ +/themes/ +/version.php +/nextcloud-init-sync.lock From c9ee4f3dc89e8c43b209b7a594ac8442574c971d Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 14:23:16 +0000 Subject: [PATCH 035/184] fix: entrypoint tls dir problem --- docker/scripts/entrypoint.sh | 5 +- docker/scripts/nextcloud/entrypoint.sh | 5 +- docker/scripts/ocmstub/entrypoint.sh | 112 +++++++++++++++++++++++++ docker/scripts/owncloud/entrypoint.sh | 5 +- 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100755 docker/scripts/ocmstub/entrypoint.sh diff --git a/docker/scripts/entrypoint.sh b/docker/scripts/entrypoint.sh index 8529ee5c..1e66afa7 100755 --- a/docker/scripts/entrypoint.sh +++ b/docker/scripts/entrypoint.sh @@ -47,7 +47,10 @@ set -euo pipefail # Define a trap to clean up resources on script exit or error. # This ensures the /tls directory is removed if something goes wrong. -trap 'rm -rf /tls' EXIT + +# @MahdiBaghbani: We don't need this, honestly, maybe we need it but I don't know +# under what circumstances would it become necessary, I'll leave it here. +# trap 'rm -rf /tls' EXIT # ----------------------------------------------------------------------------------- # Directory and Certificate Setup diff --git a/docker/scripts/nextcloud/entrypoint.sh b/docker/scripts/nextcloud/entrypoint.sh index 06ca638b..c52327fc 100755 --- a/docker/scripts/nextcloud/entrypoint.sh +++ b/docker/scripts/nextcloud/entrypoint.sh @@ -47,7 +47,10 @@ set -euo pipefail # Define a trap to clean up resources on script exit or error. # This ensures the /tls directory is removed if something goes wrong. -trap 'rm -rf /tls' EXIT + +# @MahdiBaghbani: We don't need this, honestly, maybe we need it but I don't know +# under what circumstances would it become necessary, I'll leave it here. +# trap 'rm -rf /tls' EXIT # ----------------------------------------------------------------------------------- # Directory and Certificate Setup diff --git a/docker/scripts/ocmstub/entrypoint.sh b/docker/scripts/ocmstub/entrypoint.sh new file mode 100755 index 00000000..1e66afa7 --- /dev/null +++ b/docker/scripts/ocmstub/entrypoint.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Docker Initialization Script for TLS Certificates +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script runs inside a Docker container to set up TLS certificates for a given +# host. It copies certificates and keys from a specified directory, validates their +# presence, and creates symbolic links for easy reference by the main service. +# Finally, it executes the provided command (e.g., the container's CMD). +# +# Requirements: +# - The HOST environment variable must be set. +# - The /certificates directory should contain .crt and .key files matching the HOST. +# +# Usage: +# In a Dockerfile: +# COPY entrypoint.sh /entrypoint.sh +# ENTRYPOINT ["/entrypoint.sh"] +# +# The container's CMD will be executed by this script once TLS setup is complete. +# +# Notes: +# - If HOST is "example.com", then the script expects to find "example.com.crt" and +# "example.com.key" inside the /tls directory after copying from /certificates. +# - The script creates /tls directory if it doesn't exist. +# - Symbolic links are created: +# /tls/server.crt -> /tls/${HOST}.crt +# /tls/server.key -> /tls/${HOST}.key +# +# Example: +# HOST=example.com docker run --rm -e HOST=example.com myimage:latest +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to missing HOST, missing certificates, or command execution issues. + +# ----------------------------------------------------------------------------------- +# Safety and Error Handling +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# Define a trap to clean up resources on script exit or error. +# This ensures the /tls directory is removed if something goes wrong. + +# @MahdiBaghbani: We don't need this, honestly, maybe we need it but I don't know +# under what circumstances would it become necessary, I'll leave it here. +# trap 'rm -rf /tls' EXIT + +# ----------------------------------------------------------------------------------- +# Directory and Certificate Setup +# ----------------------------------------------------------------------------------- + +# Ensure the /tls directory exists +mkdir -p /tls + +# If the /certificates directory exists, copy .crt and .key files to /tls +if [[ -d "/certificates" ]]; then + printf "Copying certificates and keys to /tls...\n" + # Copy .crt files + find /certificates -type f -name "*.crt" -exec cp -f {} /tls/ \; + + # Copy .key files + find /certificates -type f -name "*.key" -exec cp -f {} /tls/ \; + + printf "Certificate and key files copied successfully.\n" +else + printf "Warning: /certificates directory does not exist. Skipping certificate copy.\n" >&2 +fi + +# ----------------------------------------------------------------------------------- +# Validate HOST Environment Variable +# ----------------------------------------------------------------------------------- + +# Ensure the HOST environment variable is set +if [[ -z "${HOST}" ]]; then + printf "Error: HOST environment variable is not set. Aborting.\n" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Check for HOST-Specific Certificate and Key +# ----------------------------------------------------------------------------------- +crt_path="/tls/${HOST}.crt" +key_path="/tls/${HOST}.key" + +if [[ -f "${crt_path}" && -f "${key_path}" ]]; then + printf "Creating symbolic links for certificates...\n" + # Create symbolic links to /tls/server.crt and /tls/server.key + ln --symbolic --force "${crt_path}" /tls/server.crt + ln --symbolic --force "${key_path}" /tls/server.key + + printf "Symbolic links created successfully.\n" +else + printf "Error: Certificate or key file for host '%s' not found in /tls.\n" "${HOST}" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Execute the Provided Command +# ----------------------------------------------------------------------------------- + +# Print the command about to be executed for clarity +printf "Executing command: %s\n" "$*" + +# Execute the provided command with exec so that it becomes the container's main process +exec "$@" diff --git a/docker/scripts/owncloud/entrypoint.sh b/docker/scripts/owncloud/entrypoint.sh index 06ca638b..c52327fc 100755 --- a/docker/scripts/owncloud/entrypoint.sh +++ b/docker/scripts/owncloud/entrypoint.sh @@ -47,7 +47,10 @@ set -euo pipefail # Define a trap to clean up resources on script exit or error. # This ensures the /tls directory is removed if something goes wrong. -trap 'rm -rf /tls' EXIT + +# @MahdiBaghbani: We don't need this, honestly, maybe we need it but I don't know +# under what circumstances would it become necessary, I'll leave it here. +# trap 'rm -rf /tls' EXIT # ----------------------------------------------------------------------------------- # Directory and Certificate Setup From f8cadcac58d7076317d48c827daffafbfd2fb353 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 15:17:48 +0000 Subject: [PATCH 036/184] remove: unused file --- docker/scripts/ocmstub/index.js | 64 --------------------------------- 1 file changed, 64 deletions(-) delete mode 100755 docker/scripts/ocmstub/index.js diff --git a/docker/scripts/ocmstub/index.js b/docker/scripts/ocmstub/index.js deleted file mode 100755 index 7455aa96..00000000 --- a/docker/scripts/ocmstub/index.js +++ /dev/null @@ -1,64 +0,0 @@ -/* jshint ignore:start */ -const https = require("https"); -const fs = require("fs"); -const url = require("url"); - -const SERVER_NAME = process.env.HOST || "meshdir"; -const SERVER_PORT = process.env.PORT || 443; -const SERVER_HOST = `${SERVER_NAME}.docker`; - -const HTTPS_OPTIONS = { - key: fs.readFileSync(`/tls/${SERVER_NAME}.key`), cert: fs.readFileSync(`/tls/${SERVER_NAME}.crt`) -} - -function sendHTML(res, text) { - res.statusCode = 200; - res.setHeader("Content-Type", "text/html"); - res.end(`OCM Stub${text}`); -} - -const server = https.createServer(HTTPS_OPTIONS, async (req, res) => { - - let bodyIn = ""; - req.on("data", (chunk) => { - - bodyIn += chunk.toString(); - }); - req.on("end", async () => { - try { - if (req.url.startsWith("/meshdir?")) { - url.parse(req.url, true).query; - const config = { - nextcloud1: "https://nextcloud1.docker/index.php/apps/sciencemesh/accept", - nextcloud2: "https://nextcloud2.docker/index.php/apps/sciencemesh/accept", - owncloud1: "https://owncloud1.docker/index.php/apps/sciencemesh/accept", - owncloud2: "https://owncloud2.docker/index.php/apps/sciencemesh/accept", - }; - const items = []; - const scriptLines = []; - Object.keys(config).forEach(key => { - if (typeof config[key] === "string") { - items.push(` `); - scriptLines.push(` document.getElementById("${key}").setAttribute("href", "${config[key]}"+window.location.search);`); - } else { - const params = new URLSearchParams(req.url.split("?")[1]); - - const token = params.get("token"); - const providerDomain = params.get("providerDomain"); - items.push(`
  • ${key}: Please run
    ocm-invite-forward -idp ${providerDomain} -token ${token}
    in Reva's CLI tool.
  • `); - } - }) - - sendHTML(res, `Welcome to the meshdir stub. Please click a server to continue to:\n
      ${items.join("\n")}
    \n\n`); - } else { - - sendHTML(res, "'OK'"); - } - } catch (e) { - console.error(e); - } - }); -}); - -server.listen(SERVER_PORT, SERVER_HOST); -/* jshint ignore:end */ From ae2bc7a0de0b7320dd99ecdb866cb6270904f5e7 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 15:18:18 +0000 Subject: [PATCH 037/184] add: permissions for folder and use entrypoint script --- docker/dockerfiles/ocmstub.Dockerfile | 33 ++++++++++++++++++--------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/docker/dockerfiles/ocmstub.Dockerfile b/docker/dockerfiles/ocmstub.Dockerfile index 15907777..db50425a 100644 --- a/docker/dockerfiles/ocmstub.Dockerfile +++ b/docker/dockerfiles/ocmstub.Dockerfile @@ -66,16 +66,6 @@ WORKDIR /ocmstub # --production ensures only production dependencies are installed, reducing size. RUN npm ci --production -# ---------------------------------------------------------------------------- -# Install TLS Certificates -# ---------------------------------------------------------------------------- -# Copy self signed certificates and link them to OS cert directory and update -# the systems trusted certificates -COPY ./tls/certificates/* /tls/ -COPY ./tls/certificate-authority/* /tls/ -RUN ln --symbolic --force /tls/*.crt /usr/local/share/ca-certificates; \ - update-ca-certificates - # ---------------------------------------------------------------------------- # Expose Ports # ---------------------------------------------------------------------------- @@ -89,11 +79,24 @@ EXPOSE 443/tcp # This is helpful for local testing but should not be used in production environments. ENV NODE_TLS_REJECT_UNAUTHORIZED=0 +# ---------------------------------------------------------------------------- +# Install TLS Certificates +# ---------------------------------------------------------------------------- +# Copy self signed certificates and link them to OS cert directory and update +# the systems trusted certificates +COPY ./tls/certificates/* /tls/ +COPY ./tls/certificate-authority/* /tls/ + # ---------------------------------------------------------------------------- # Switch to Non-Root User # ---------------------------------------------------------------------------- # The base Node image provides a 'node' user. We'll run as 'node' for better security. -RUN chown -R node:node /ocmstub +RUN chown -R node:root /ocmstub; \ + chmod -R g=u /ocmstub; \ + chown -R node:root /tls; \ + chmod -R g=u /tls; \ + ln --symbolic --force /tls/*.crt /usr/local/share/ca-certificates; \ + update-ca-certificates USER node # ---------------------------------------------------------------------------- @@ -103,8 +106,16 @@ USER node HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -k -f https://localhost:443 || exit 1 + +# ---------------------------------------------------------------------------- +# Add required scripts +# ---------------------------------------------------------------------------- +# Scripts such as entrypoint.sh +COPY ./scripts/ocmstub/*.sh / + # ---------------------------------------------------------------------------- # Startup Command # ---------------------------------------------------------------------------- # Finally, run the Node.js application defined in stub.js. +ENTRYPOINT ["/entrypoint.sh"] CMD ["node", "stub.js"] From a34916ed2da580a272ea9bb4dc17ca3b282379b6 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 15:54:16 +0000 Subject: [PATCH 038/184] remove: seafile related scripts --- .../share-with/nextcloud-seafile.sh | 70 ----- .../share-with/owncloud-seafile.sh | 69 ----- .../share-with/seafile-ocmstub.sh | 286 ------------------ 3 files changed, 425 deletions(-) delete mode 100755 dev/ocm-test-suite/share-with/nextcloud-seafile.sh delete mode 100755 dev/ocm-test-suite/share-with/owncloud-seafile.sh delete mode 100755 dev/ocm-test-suite/share-with/seafile-ocmstub.sh diff --git a/dev/ocm-test-suite/share-with/nextcloud-seafile.sh b/dev/ocm-test-suite/share-with/nextcloud-seafile.sh deleted file mode 100755 index 5891857b..00000000 --- a/dev/ocm-test-suite/share-with/nextcloud-seafile.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"11.0.5"} - -# seafile version: -# - 8.0.8 -# - 9.0.10 -# - 10.0.1 -# - 11.0.5 -EFSS_PLATFORM_2_VERSION=${2:-"11.0.5"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function waitForCollabora() { - x=$(docker logs collabora.docker | grep -c "Ready") - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo "Waiting for Collabora to be ready, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker logs collabora.docker | grep -c "Ready") - done - redirect_to_null_cmd echo "Collabora is ready" -} diff --git a/dev/ocm-test-suite/share-with/owncloud-seafile.sh b/dev/ocm-test-suite/share-with/owncloud-seafile.sh deleted file mode 100755 index fd0ef921..00000000 --- a/dev/ocm-test-suite/share-with/owncloud-seafile.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"11.0.5"} - -# seafile version: -# - 8.0.8 -# - 9.0.10 -# - 10.0.1 -# - 11.0.5 -EFSS_PLATFORM_2_VERSION=${2:-"11.0.5"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function waitForCollabora() { - x=$(docker logs collabora.docker | grep -c "Ready") - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo "Waiting for Collabora to be ready, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker logs collabora.docker | grep -c "Ready") - done - redirect_to_null_cmd echo "Collabora is ready" -} diff --git a/dev/ocm-test-suite/share-with/seafile-ocmstub.sh b/dev/ocm-test-suite/share-with/seafile-ocmstub.sh deleted file mode 100755 index b9780091..00000000 --- a/dev/ocm-test-suite/share-with/seafile-ocmstub.sh +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# seafile version: -# - 8.0.8 -# - 9.0.10 -# - 10.0.1 -# - 11.0.5 -EFSS_PLATFORM_1_VERSION=${1:-"11.0.5"} - -# ocmstub version: -# - 1.0 -EFSS_PLATFORM_2_VERSION=${2:-"1.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function waitForCollabora() { - x=$(docker logs collabora.docker | grep -c "Ready") - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo "Waiting for Collabora to be ready, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker logs collabora.docker | grep -c "Ready") - done - redirect_to_null_cmd echo "Collabora is ready" -} - -function createEfssSeafile() { - local platform="${1}" - local number="${2}" - local user_email="${3}" - local password="${4}" - local remote_ocm_server="${5}" - local tag="${6-latest}" - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="memcache${platform}${number}.docker" \ - memcached:1.6.18 \ - memcached -m 256 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - -e TIME_ZONE="Etc/UTC" \ - -e DB_HOST="maria${platform}${number}.docker" \ - -e DB_ROOT_PASSWD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" \ - -e SEAFILE_ADMIN_EMAIL="${user_email}" \ - -e SEAFILE_ADMIN_PASSWORD="${password}" \ - -e SEAFILE_SERVER_LETSENCRYPT=false \ - -e FORCE_HTTPS_IN_CONF=false \ - -e SEAFILE_SERVER_HOSTNAME="${platform}${number}.docker" \ - -e SEAFILE_MEMCACHE_HOST="memcache${platform}${number}.docker" \ - -e SEAFILE_MEMCACHE_PORT=11211 \ - -v "${ENV_ROOT}/temp/sea-init.sh:/init.sh" \ - -v "${ENV_ROOT}/temp/seafile-data/${platform}${number}:/shared" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/shared/ssl/${platform}${number}.docker.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/shared/ssl/${platform}${number}.docker.key" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - "seafileltd/seafile-mc:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - - # seafile needs time to bootstrap itself. - sleep 5 - - # run init script inside seafile. - redirect_to_null_cmd docker exec -e remote_ocm_server="${remote_ocm_server}" "${platform}${number}.docker" bash -c "/init.sh ${remote_ocm_server}" - - # restart seafile to apply our changes. - sleep 2 - redirect_to_null_cmd docker restart "${platform}${number}.docker" - sleep 2 - - redirect_to_null_cmd echo "" -} - -function createOcmStub() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - echo docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/tls/${platform}${number}.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/tls/${platform}${number}.key" \ - -e HOST="${platform}${number}" \ - "${image}:${tag}" - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/tls/${platform}${number}.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/tls/${platform}${number}.key" \ - -e HOST="${platform}${number}" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "${platform}${number}.docker" 443 - - redirect_to_null_cmd echo "" -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/seafile.sh" "${ENV_ROOT}/temp/sea-init.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - - -############### -### Seafile ### -############### - -# Seafiles. -createEfssSeafile seafile 1 jonathan@seafile.com xu seafile2 "${EFSS_PLATFORM_1_VERSION}" -createOcmStub ocmstub 2 michiel dejong ocmstub.sh "${EFSS_PLATFORM_2_VERSION}" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc_auto.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://seafile1.docker -> username: jonathan@seafile.com password: xu" - echo "https://ocmstub2.docker/? -> just click 'Log in'" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with-signed-http/seafile-${P1_VER}-to-seafile-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi From c4ec3673506c326d185860fd0abf6db3788d6677 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 16:11:01 +0000 Subject: [PATCH 039/184] refactor: share with scripts with modular functions and documentations --- .../share-with/nextcloud-nextcloud.sh | 15 +- .../share-with/nextcloud-ocmstub.sh | 678 +++++++++++------- .../share-with/nextcloud-owncloud.sh | 6 +- .../share-with/ocmstub-ocmstub.sh | 554 +++++++++----- .../share-with/owncloud-nextcloud.sh | 6 +- .../share-with/owncloud-ocmstub.sh | 676 ++++++++++------- .../share-with/owncloud-owncloud.sh | 15 +- 7 files changed, 1255 insertions(+), 695 deletions(-) diff --git a/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh index fe931ddb..ab305807 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh @@ -39,7 +39,8 @@ set -euo pipefail # ----------------------------------------------------------------------------------- # Default versions -DEFAULT_EFSS_VERSION="v27.1.11" +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v27.1.11" DEFAULT_SCRIPT_MODE="dev" DEFAULT_BROWSER_PLATFORM="electron" @@ -260,8 +261,8 @@ create_nextcloud() { # $@ - Command-line arguments # ----------------------------------------------------------------------------------- parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_VERSION}" + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" } @@ -316,10 +317,10 @@ main() { docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." fi - # Create Nextcloud containers - # # id # username # password # image # tag - create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" - create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then echo "Setting up development environment..." diff --git a/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh index 609ce4aa..7abed4dc 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh @@ -1,270 +1,452 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# ocmstub version: -# - v1.0 -EFSS_PLATFORM_2_VERSION=${2:-"1.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-ocmstub.sh v28.0.14 v1.0.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v1.0.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containerS +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." fi + done } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) done - redirect_to_null_cmd echo "${1} port ${2} is open" + run_quietly_if_ci echo "Port ${port} is now open on ${container}." } -function createNextcloud() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 else - local image="pondersource/dev-stock-${platform}-${image}" + "$@" fi +} - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" } -function createOcmStub() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" fi +} - redirect_to_null_cmd echo "creating efss ${platform} ${number}" +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/tls/${platform}${number}.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/tls/${platform}${number}.key" \ - -e HOST="${platform}${number}" \ - "${image}:${tag}" +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} - # wait for hostname port to be open. - waitForPort "${platform}${number}.docker" 443 - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################# -### Nextcloud ### -################# - -# syntax: -# createNextcloud platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# EFSSs. -createNextcloud nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" - -# ocmstub only has the latest tag so we don't need this "${EFSS_PLATFORM_VERSION}" -createOcmStub ocmstub 1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc_auto.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://ocmstub1.docker/? -> just click 'Log in'" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://ocmstub1.docker (just click 'Log in')" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh index 746ee350..754edf11 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh @@ -371,9 +371,9 @@ main() { fi # Create EFSS containers - # # id # username # password # image # tag - create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" - create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then echo "Setting up development environment..." diff --git a/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh b/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh index 9f0010ce..5cc0dfca 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh @@ -1,199 +1,391 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# ocmstub version: -# - v1.0.0 -EFSS_PLATFORM_1_VERSION=${1:-"1.0"} -EFSS_PLATFORM_2_VERSION=${2:-"1.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" +# ----------------------------------------------------------------------------------- +# Script to Test OcmStub to OcmStub OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./ocmstub-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v1.0.0"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./ocmstub-ocmstub.sh v1.0.0 v1.0.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v1.0.0" +DEFAULT_EFSS_2_VERSION="v1.0.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containerS +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." fi + done } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) done - redirect_to_null_cmd echo "${1} port ${2} is open" + run_quietly_if_ci echo "Port ${port} is now open on ${container}." } -function createOcmStub() { - local platform="${1}" - local number="${2}" - local tag="${3-latest}" - local image="${4}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 else - local image="pondersource/dev-stock-${platform}-${image}" + "$@" fi +} + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} - redirect_to_null_cmd echo "creating efss ${platform} ${number}" +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/tls/${platform}${number}.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/tls/${platform}${number}.key" \ - "${image}:${tag}" +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} - # wait for hostname port to be open. - waitForPort "${platform}${number}.docker" 443 - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################# -### ocmstub ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: ocmstub. -# number: should be unique for each platform, for example: you cannot have two ocmstubs with same number. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ocmstub only has the latest tag so we don't need this "${EFSS_PLATFORM_VERSION}" -createOcmStub ocmstub 1 -createOcmStub ocmstub 2 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://ocmstub1.docker/? -> just click 'Log in'" - echo "https://ocmstub2.docker/? -> just click 'Log in'" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/ocmstub-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + create_ocmstub 2 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://ocmstub1.docker (just click 'Log in')" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/ocmstub-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi diff --git a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh index 8a8efbed..f8be8dd5 100755 --- a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh @@ -371,9 +371,9 @@ main() { fi # Create EFSS containers - # # id # username # password # image # tag - create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" - create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + # # id # username # password # image # tag + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then echo "Setting up development environment..." diff --git a/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh b/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh index a7c386ce..9fc64218 100755 --- a/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh @@ -1,268 +1,452 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} - -# ocmstub version: -# - v1.0 -EFSS_PLATFORM_2_VERSION=${2:-"1.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" +# ----------------------------------------------------------------------------------- +# Script to Test ownCloud to OcmStub OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as ownCloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v10.15.0"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-ocmstub.sh v10.15.0 v1.0.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v10.15.0" +DEFAULT_EFSS_2_VERSION="v1.0.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containerS +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." fi + done } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) done - redirect_to_null_cmd echo "${1} port ${2} is open" + run_quietly_if_ci echo "Port ${port} is now open on ${container}." } -function createOwnCloud() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 else - local image="pondersource/dev-stock-${platform}-${image}" + "$@" fi +} - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" } -function createOcmStub() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" fi +} - redirect_to_null_cmd echo "creating efss ${platform} ${number}" +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/tls/${platform}${number}.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/tls/${platform}${number}.key" \ - -e HOST="${platform}${number}" \ - "${image}:${tag}" +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_owncloud +# Purpose: Create a ownCloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_owncloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaowncloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaowncloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="owncloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="owncloud${number}" \ + -e OWNCLOUD_HOST="owncloud${number}.docker" \ + -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ + -e OWNCLOUD_ADMIN_USER="${user}" \ + -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ + -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="mariaowncloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} - # wait for hostname port to be open. - waitForPort "${platform}${number}.docker" 443 - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sm-ocm.sh" "${ENV_ROOT}/temp/owncloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################ -### ownCloud ### -################ - -# syntax: -# createOwnCloud platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownClouds. -createOwnCloud owncloud 1 marie radioactivity owncloud.sh latest ocm-test-suite - -# ocmstub only has the latest tag so we don't need this "${EFSS_PLATFORM_VERSION}" -createOcmStub ocmstub 1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc_auto.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://ocmstub1.docker/? -> just click 'Log in'" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/owncloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://ocmstub1.docker (just click 'Log in')" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/owncloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/share-with/owncloud-owncloud.sh b/dev/ocm-test-suite/share-with/owncloud-owncloud.sh index 0ea4621a..0756716d 100755 --- a/dev/ocm-test-suite/share-with/owncloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-owncloud.sh @@ -39,7 +39,8 @@ set -euo pipefail # ----------------------------------------------------------------------------------- # Default versions -DEFAULT_EFSS_VERSION="v10.15.0" +DEFAULT_EFSS_1_VERSION="v10.15.0" +DEFAULT_EFSS_2_VERSION="v10.15.0" DEFAULT_SCRIPT_MODE="dev" DEFAULT_BROWSER_PLATFORM="electron" @@ -260,8 +261,8 @@ create_owncloud() { # $@ - Command-line arguments # ----------------------------------------------------------------------------------- parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_VERSION}" + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" } @@ -316,10 +317,10 @@ main() { docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." fi - # Create ownCloud containers - # # id # username # password # image # tag - create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" - create_owncloud 2 "mahdi" "baghbani" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + # Create EFSS containers + # # id # username # password # image # tag + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 2 "mahdi" "baghbani" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then echo "Setting up development environment..." From 3489fb9aec0f01f3f3fc3496f48419bd94f46f13 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 16:36:55 +0000 Subject: [PATCH 040/184] [no ci] fix: typo --- dev/ocm-test-suite/share-with/owncloud-ocmstub.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh b/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh index 9fc64218..546bf951 100755 --- a/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh @@ -12,7 +12,7 @@ # It supports both development and CI environments, with optional browser support. # Usage: -# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# ./owncloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] # Arguments: # EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v10.15.0"). @@ -26,7 +26,7 @@ # - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. # Example: -# ./nextcloud-ocmstub.sh v10.15.0 v1.0.0 ci electron +# ./owncloud-ocmstub.sh v10.15.0 v1.0.0 ci electron # ----------------------------------------------------------------------------------- From 387249f659e4b808e45265d832a9a20a28c79c43 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 16:41:34 +0000 Subject: [PATCH 041/184] fix: typo --- dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh | 2 +- dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh | 2 +- dev/ocm-test-suite/share-with/nextcloud-owncloud.sh | 2 +- dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh | 2 +- dev/ocm-test-suite/share-with/owncloud-nextcloud.sh | 2 +- dev/ocm-test-suite/share-with/owncloud-ocmstub.sh | 2 +- dev/ocm-test-suite/share-with/owncloud-owncloud.sh | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh index ab305807..842319c6 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh @@ -55,7 +55,7 @@ TEMP_DIR="temp" TLS_CA_DIR="docker/tls/certificate-authority" TLS_CERTIFICATES_DIR="docker/tls/certificates" -# 3rd party containerS +# 3rd party containers CYPRESS_REPO=cypress/included CYPRESS_TAG=13.13.1 FIREFOX_REPO=jlesage/firefox diff --git a/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh index 7abed4dc..0997c4c7 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh @@ -55,7 +55,7 @@ TEMP_DIR="temp" TLS_CA_DIR="docker/tls/certificate-authority" TLS_CERTIFICATES_DIR="docker/tls/certificates" -# 3rd party containerS +# 3rd party containers CYPRESS_REPO=cypress/included CYPRESS_TAG=13.13.1 FIREFOX_REPO=jlesage/firefox diff --git a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh index 754edf11..86c93d7e 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh @@ -55,7 +55,7 @@ TEMP_DIR="temp" TLS_CA_DIR="docker/tls/certificate-authority" TLS_CERTIFICATES_DIR="docker/tls/certificates" -# 3rd party containerS +# 3rd party containers CYPRESS_REPO=cypress/included CYPRESS_TAG=13.13.1 FIREFOX_REPO=jlesage/firefox diff --git a/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh b/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh index 5cc0dfca..f604fb11 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh @@ -52,7 +52,7 @@ TEMP_DIR="temp" TLS_CA_DIR="docker/tls/certificate-authority" TLS_CERTIFICATES_DIR="docker/tls/certificates" -# 3rd party containerS +# 3rd party containers CYPRESS_REPO=cypress/included CYPRESS_TAG=13.13.1 FIREFOX_REPO=jlesage/firefox diff --git a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh index f8be8dd5..83cf9873 100755 --- a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh @@ -55,7 +55,7 @@ TEMP_DIR="temp" TLS_CA_DIR="docker/tls/certificate-authority" TLS_CERTIFICATES_DIR="docker/tls/certificates" -# 3rd party containerS +# 3rd party containers CYPRESS_REPO=cypress/included CYPRESS_TAG=13.13.1 FIREFOX_REPO=jlesage/firefox diff --git a/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh b/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh index 546bf951..54daaa96 100755 --- a/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh @@ -55,7 +55,7 @@ TEMP_DIR="temp" TLS_CA_DIR="docker/tls/certificate-authority" TLS_CERTIFICATES_DIR="docker/tls/certificates" -# 3rd party containerS +# 3rd party containers CYPRESS_REPO=cypress/included CYPRESS_TAG=13.13.1 FIREFOX_REPO=jlesage/firefox diff --git a/dev/ocm-test-suite/share-with/owncloud-owncloud.sh b/dev/ocm-test-suite/share-with/owncloud-owncloud.sh index 0756716d..3e1c6ac7 100755 --- a/dev/ocm-test-suite/share-with/owncloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-owncloud.sh @@ -55,7 +55,7 @@ TEMP_DIR="temp" TLS_CA_DIR="docker/tls/certificate-authority" TLS_CERTIFICATES_DIR="docker/tls/certificates" -# 3rd party containerS +# 3rd party containers CYPRESS_REPO=cypress/included CYPRESS_TAG=13.13.1 FIREFOX_REPO=jlesage/firefox From 841f1a7785e0e8ea14502c977a5c4a248c8cc634 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 17:19:54 +0000 Subject: [PATCH 042/184] [no ci] add: mermaid flowchart for the ocm test suite --- README.md | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/README.md b/README.md index 04cdf662..5f07a239 100644 --- a/README.md +++ b/README.md @@ -364,3 +364,180 @@ See [docs](./docs/xdebug.md) # SOLID RemoteStorage for development see [docs](./docs/solid-remotestorage.md) + + +```mermaid +flowchart TD + %% Start of the workflow + A[GitHub Actions Triggered] --> B[Checkout Repository] + + %% Pull Docker Images + B --> C[Pull Docker Images] + C --> C1[Pull MariaDB:11.4.2] + C --> C2[Pull Cypress:13.13.1] + C --> C3[Pull pondersource/dev-stock-nextcloud:v27.1.11] + + %% Initialize Environment + C3 --> D[Initialize Environment] + D --> D1[Resolve Script Directory] + D --> D2[Set ENV_ROOT] + D --> D3[Export ENV_ROOT] + + %% Validate Files and Directories + D --> E[Validate Required Files & Directories] + E --> E1[Check TLS Certificates] + E --> E2[Check Cypress Configuration] + + %% Parse Arguments + E --> F[Parse Command-Line Arguments] + F --> F1[Set EFSS_PLATFORM_1_VERSION] + F --> F2[Set EFSS_PLATFORM_2_VERSION] + F --> F3[Set SCRIPT_MODE dev/ci] + F --> F4[Set BROWSER_PLATFORM electron/chrome/edge/firefox] + + %% Clean Up Previous Resources + F --> G[Clean Up Previous Resources] + G --> G1[Run clean.sh] + G --> G2[Remove Temporary Directories] + G --> G3[Remove Old Docker Containers] + + %% Create Docker Network + G --> H[Create Docker Network: testnet] + H --> H1{Does testnet exist?} + H1 -- Yes --> H2[Use Existing testnet] + H1 -- No --> H3[Create testnet] + + %% Start MariaDB Container + H2 --> I[Start MariaDB Container] + I --> I1[Configure MariaDB Environment Variables] + I --> I2[Connect to testnet] + I --> I3[Wait for MariaDB Port 3306] + + %% Start Nextcloud Containers + I --> J[Start Nextcloud Containers] + J --> J1[Start nextcloud1.docker] + J1 --> J1a[Configure Nextcloud1 with MariaDB] + J1 --> J1b[Set Admin Credentials einstein/relativity] + J1 --> J1c[Wait for Nextcloud1 Port 443] + + J --> J2[Start nextcloud2.docker] + J2 --> J2a[Configure Nextcloud2 with MariaDB] + J2 --> J2b[Set Admin Credentials michiel/dejong] + J2 --> J2c[Wait for Nextcloud2 Port 443] + + %% Conditional: Dev Mode vs CI Mode + J2c --> K{SCRIPT_MODE} + K -- Dev --> L[Start Dev Mode Containers] + K -- CI --> M[Start CI Mode Containers] + + %% Dev Mode Setup + L --> L1[Start Firefox Container] + L --> L2[Start VNC Server Container] + L --> L3[Start Cypress Container in Dev Mode] + + %% Provide Dev Mode Instructions + L3 --> N[Provide Dev Mode Access Instructions] + N --> O[Run Cypress Tests in Dev Mode] + + %% CI Mode Setup + M --> M1[Configure Cypress for CI Mode] + M1 --> M2[Run Cypress Tests in CI Mode] + + %% Cypress Test Execution + O --> P[Cypress Executes Federated Share Test Suite] + M2 --> P + + %% Verify Test Results + P --> Q{Test Success?} + Q -- Yes --> R[Mark as Passed] + Q -- No --> S[Mark as Failed] + + %% Cleanup Resources + R --> T[Cleanup Resources] + S --> T + T --> U[End Workflow] + + %% Subgraphs for better organization + subgraph Docker Image Pulling + C1 + C2 + C3 + end + + subgraph Environment Initialization + D1 + D2 + D3 + end + + subgraph File Validation + E1 + E2 + end + + subgraph Argument Parsing + F1 + F2 + F3 + F4 + end + + subgraph Resource Cleanup + G1 + G2 + G3 + end + + subgraph Network Setup + H1 + H2 + H3 + end + + subgraph MariaDB Setup + I1 + I2 + I3 + end + + subgraph Nextcloud Setup + J1a + J1b + J1c + J2a + J2b + J2c + end + + subgraph Dev Mode Containers + L1 + L2 + L3 + end + + subgraph CI Mode Containers + M1 + M2 + end + + subgraph Test Execution + P + end + + subgraph Test Results + Q + R + S + end + + subgraph Final Cleanup + T + U + end + + %% Style Adjustments + classDef success fill:#d4edda,stroke:#28a745,stroke-width:2px; + classDef failure fill:#f8d7da,stroke:#dc3545,stroke-width:2px; + class R success; + class S failure; +``` \ No newline at end of file From 2c979023a4d1464a43f8c972c5e55dfc56be7e67 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 17:37:08 +0000 Subject: [PATCH 043/184] fix: break it down into multiple diagrams --- README.md | 124 +++++++++++++++--------------------------------------- 1 file changed, 33 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 5f07a239..d302282e 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,7 @@ See [docs](./docs/xdebug.md) for development see [docs](./docs/solid-remotestorage.md) +### 1. Initial Setup (GitHub Actions Trigger, Docker Pulls, and Environment Initialization) ```mermaid flowchart TD %% Start of the workflow @@ -387,13 +388,17 @@ flowchart TD D --> E[Validate Required Files & Directories] E --> E1[Check TLS Certificates] E --> E2[Check Cypress Configuration] +``` +### 2. Docker Network and Container Management +```mermaid +flowchart TD %% Parse Arguments - E --> F[Parse Command-Line Arguments] + E[Validate Required Files & Directories] --> F[Parse Command-Line Arguments] F --> F1[Set EFSS_PLATFORM_1_VERSION] F --> F2[Set EFSS_PLATFORM_2_VERSION] - F --> F3[Set SCRIPT_MODE dev/ci] - F --> F4[Set BROWSER_PLATFORM electron/chrome/edge/firefox] + F --> F3[Set SCRIPT_MODE - dev/ci] + F --> F4[Set BROWSER_PLATFORM - electron/chrome/edge/firefox] %% Clean Up Previous Resources F --> G[Clean Up Previous Resources] @@ -406,9 +411,12 @@ flowchart TD H --> H1{Does testnet exist?} H1 -- Yes --> H2[Use Existing testnet] H1 -- No --> H3[Create testnet] +``` +```mermaid +flowchart TD %% Start MariaDB Container - H2 --> I[Start MariaDB Container] + H2[Use Existing testnet] --> I[Start MariaDB Container] I --> I1[Configure MariaDB Environment Variables] I --> I2[Connect to testnet] I --> I3[Wait for MariaDB Port 3306] @@ -417,16 +425,20 @@ flowchart TD I --> J[Start Nextcloud Containers] J --> J1[Start nextcloud1.docker] J1 --> J1a[Configure Nextcloud1 with MariaDB] - J1 --> J1b[Set Admin Credentials einstein/relativity] + J1 --> J1b[Set Admin Credentials - einstein/relativity] J1 --> J1c[Wait for Nextcloud1 Port 443] J --> J2[Start nextcloud2.docker] J2 --> J2a[Configure Nextcloud2 with MariaDB] - J2 --> J2b[Set Admin Credentials michiel/dejong] + J2 --> J2b[Set Admin Credentials - michiel/dejong] J2 --> J2c[Wait for Nextcloud2 Port 443] +``` +### 3. Dev Mode and CI Mode Execution +```mermaid +flowchart TD %% Conditional: Dev Mode vs CI Mode - J2c --> K{SCRIPT_MODE} + J2c[Wait for Nextcloud2 Port 443] --> K{SCRIPT_MODE} K -- Dev --> L[Start Dev Mode Containers] K -- CI --> M[Start CI Mode Containers] @@ -442,102 +454,32 @@ flowchart TD %% CI Mode Setup M --> M1[Configure Cypress for CI Mode] M1 --> M2[Run Cypress Tests in CI Mode] +``` +### 4. Cypress Test Execution and Result Verification +```mermaid +flowchart TD %% Cypress Test Execution - O --> P[Cypress Executes Federated Share Test Suite] - M2 --> P + O[Run Cypress Tests in Dev Mode] --> P[Cypress Executes Open Cloud Mesh Test Suite] + M2[Run Cypress Tests in CI Mode] --> P %% Verify Test Results P --> Q{Test Success?} Q -- Yes --> R[Mark as Passed] Q -- No --> S[Mark as Failed] +``` - %% Cleanup Resources - R --> T[Cleanup Resources] - S --> T +### 5. Final Cleanup and Workflow Conclusion +```mermaid +flowchart TD + %% Final Cleanup + R[Mark as Passed] --> T[Cleanup Resources] + S[Mark as Failed] --> T T --> U[End Workflow] %% Subgraphs for better organization - subgraph Docker Image Pulling - C1 - C2 - C3 - end - - subgraph Environment Initialization - D1 - D2 - D3 - end - - subgraph File Validation - E1 - E2 - end - - subgraph Argument Parsing - F1 - F2 - F3 - F4 - end - - subgraph Resource Cleanup - G1 - G2 - G3 - end - - subgraph Network Setup - H1 - H2 - H3 - end - - subgraph MariaDB Setup - I1 - I2 - I3 - end - - subgraph Nextcloud Setup - J1a - J1b - J1c - J2a - J2b - J2c - end - - subgraph Dev Mode Containers - L1 - L2 - L3 - end - - subgraph CI Mode Containers - M1 - M2 - end - - subgraph Test Execution - P - end - - subgraph Test Results - Q - R - S - end - - subgraph Final Cleanup - T - U - end - - %% Style Adjustments classDef success fill:#d4edda,stroke:#28a745,stroke-width:2px; classDef failure fill:#f8d7da,stroke:#dc3545,stroke-width:2px; class R success; class S failure; -``` \ No newline at end of file +``` From 938e5077f58febc5ad60b3bab8b740fb46d731ab Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 17:42:33 +0000 Subject: [PATCH 044/184] [no ci] fix: text color --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d302282e..279d2b8e 100644 --- a/README.md +++ b/README.md @@ -482,4 +482,8 @@ flowchart TD classDef failure fill:#f8d7da,stroke:#dc3545,stroke-width:2px; class R success; class S failure; + + %% Modify text color to black for specific nodes + style R fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#000000 + style S fill:#f8d7da,stroke:#dc3545,stroke-width:2px,color:#000000 ``` From 80a820ee590e9e6d85d6697d700f654c525cd455 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 17:55:11 +0000 Subject: [PATCH 045/184] [no ci] add: some headlines --- README.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 279d2b8e..c68792f1 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,12 @@ See [docs](./docs/xdebug.md) for development see [docs](./docs/solid-remotestorage.md) +# OCM Test Suite Documents + +This is still a work in progress and will be moved to other place, for now I like to access it here. + + +## Flow graph of complete OCM Test suite flow for share-with between Nextcloud and Nextcloud ### 1. Initial Setup (GitHub Actions Trigger, Docker Pulls, and Environment Initialization) ```mermaid flowchart TD @@ -438,7 +444,8 @@ flowchart TD ```mermaid flowchart TD %% Conditional: Dev Mode vs CI Mode - J2c[Wait for Nextcloud2 Port 443] --> K{SCRIPT_MODE} + J1c[Wait for Nextcloud1 Port 443] --> K{SCRIPT_MODE} + J2c[Wait for Nextcloud2 Port 443] --> K K -- Dev --> L[Start Dev Mode Containers] K -- CI --> M[Start CI Mode Containers] @@ -448,8 +455,11 @@ flowchart TD L --> L3[Start Cypress Container in Dev Mode] %% Provide Dev Mode Instructions - L3 --> N[Provide Dev Mode Access Instructions] - N --> O[Run Cypress Tests in Dev Mode] + L1 --> N[Provide Dev Mode Access Instructions] + L2 --> N + L3 --> N + N --> O1[Run Cypress Tests in Dev Mode via VNC on port 5700] + N --> O2[Manually Access Containers via Firefox on port 5800] %% CI Mode Setup M --> M1[Configure Cypress for CI Mode] @@ -467,6 +477,16 @@ flowchart TD P --> Q{Test Success?} Q -- Yes --> R[Mark as Passed] Q -- No --> S[Mark as Failed] + + %% Subgraphs for better organization + classDef success fill:#d4edda,stroke:#28a745,stroke-width:2px; + classDef failure fill:#f8d7da,stroke:#dc3545,stroke-width:2px; + class R success; + class S failure; + + %% Modify text color to black for specific nodes + style R fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#000000 + style S fill:#f8d7da,stroke:#dc3545,stroke-width:2px,color:#000000 ``` ### 5. Final Cleanup and Workflow Conclusion From c34bb103650e6dc695799ce69b6691e17d7beb20 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 20 Dec 2024 18:48:08 +0000 Subject: [PATCH 046/184] refactor: pull and push scripts --- docker/pull/all.sh | 165 +++++++++++++++++++++++++++++++++++---------- docker/push/all.sh | 118 +++++++++++++++++++++++++------- 2 files changed, 221 insertions(+), 62 deletions(-) diff --git a/docker/pull/all.sh b/docker/pull/all.sh index 4e5b6223..b557d236 100755 --- a/docker/pull/all.sh +++ b/docker/pull/all.sh @@ -1,38 +1,131 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# 3rd party images. -docker pull redis:latest -docker pull mariadb:11.4.2 -docker pull memcached:1.6.18 -docker pull theasp/novnc:latest -docker pull rclone/rclone:latest -docker pull collabora/code:latest -docker pull jlesage/firefox:latest -docker pull cypress/included:13.13.1 -docker pull cs3org/wopiserver:latest -docker pull seafileltd/seafile-mc:11.0.5 -docker pull quay.io/keycloak/keycloak:latest - -# dev-stock images. -docker pull pondersource/dev-stock-ocmstub:latest -docker pull pondersource/dev-stock-revad:latest -docker pull pondersource/dev-stock-php-base:latest -docker pull pondersource/dev-stock-nextcloud:latest -docker pull pondersource/dev-stock-nextcloud:v30.0.2 -docker pull pondersource/dev-stock-nextcloud:v39.0.8 -docker pull pondersource/dev-stock-nextcloud:v28.0.14 -docker pull pondersource/dev-stock-nextcloud:v27.1.11 -# docker pull pondersource/dev-stock-nextcloud-sunet:latest -# docker pull pondersource/dev-stock-simple-saml-php:latest -docker pull pondersource/dev-stock-nextcloud-solid:latest -docker pull pondersource/dev-stock-nextcloud-sciencemesh:latest -docker pull pondersource/dev-stock-owncloud:latest -docker pull pondersource/dev-stock-owncloud-sciencemesh:latest -docker pull pondersource/dev-stock-owncloud-surf-trashbin:latest -docker pull pondersource/dev-stock-owncloud-token-based-access:latest -docker pull pondersource/dev-stock-owncloud-opencloudmesh:latest -docker pull pondersource/dev-stock-owncloud-federatedgroups:latest -docker pull pondersource/dev-stock-owncloud-ocm-test-suite:latest +# ----------------------------------------------------------------------------------- +# Docker Pull Script for PonderSource Development Images +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- +# This script pulls various Docker images for the PonderSource development environment, +# including both third-party and PonderSource-specific images. It ensures that necessary +# Docker images are pulled from specified repositories for continuous integration, +# development, and testing purposes. +# +# The script allows users to control the execution mode via a command-line argument +# (either 'dev' or 'ci'). In 'ci' mode, the output is suppressed to avoid clutter. +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Exit Immediately if a Command Fails +# ----------------------------------------------------------------------------------- +# Set the script to exit on the first error. This ensures that if any command fails, +# the script will stop and prevent further execution with potentially broken state. +# The `pipefail` option ensures that if a command in a pipeline fails, the whole pipeline +# will return a non-zero exit status. +set -eo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default execution mode is 'dev', which shows full output. +DEFAULT_SCRIPT_MODE="dev" + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout if in CI mode. +# Arguments: +# $@ - The command and its arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 # Suppress both stdout and stderr in CI mode. + else + "$@" # Run the command normally if not in CI mode. + fi +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + SCRIPT_MODE="${1:-$DEFAULT_SCRIPT_MODE}" # Default to 'dev' if no argument is provided. +} + +# ----------------------------------------------------------------------------------- +# Third-Party Docker Image Repositories and Tags +# ----------------------------------------------------------------------------------- +CYPRESS_REPO="cypress/included" +CYPRESS_TAG="13.13.1" +FIREFOX_REPO="jlesage/firefox" +FIREFOX_TAG="v24.11.1" +MARIADB_REPO="mariadb" +MARIADB_TAG="11.4.4" +VNC_REPO="theasp/novnc" +VNC_TAG="latest" +REDIS_REPO="redis" +REDIS_TAG="latest" +MEMCACHED_REPO="memcached" +MEMCACHED_TAG="1.6.18" +RCLONE_REPO="rclone/rclone" +RCLONE_TAG="latest" +COLLABORA_REPO="collabora/code" +COLLABORA_TAG="latest" +WOPISERVER_REPO="cs3org/wopiserver" +WOPISERVER_TAG="latest" +SEAFILE_MC_REPO="seafileltd/seafile-mc" +SEAFILE_MC_TAG="11.0.5" +KEYCLOAK_REPO="quay.io/keycloak/keycloak" +KEYCLOAK_TAG="latest" + +# ----------------------------------------------------------------------------------- +# Parse Command-Line Arguments +# ----------------------------------------------------------------------------------- +parse_arguments "$@" # Parse any arguments passed to the script. + +# ----------------------------------------------------------------------------------- +# Pull Third-Party Docker Images +# ----------------------------------------------------------------------------------- +run_quietly_if_ci echo "Pulling third-party Docker images..." + +# Use run_quietly_if_ci to suppress output in CI mode +run_quietly_if_ci docker pull "${REDIS_REPO}:${REDIS_TAG}" +run_quietly_if_ci docker pull "${MEMCACHED_REPO}:${MEMCACHED_TAG}" +run_quietly_if_ci docker pull "${RCLONE_REPO}:${RCLONE_TAG}" +run_quietly_if_ci docker pull "${COLLABORA_REPO}:${COLLABORA_TAG}" +run_quietly_if_ci docker pull "${VNC_REPO}:${VNC_TAG}" +run_quietly_if_ci docker pull "${CYPRESS_REPO}:${CYPRESS_TAG}" +run_quietly_if_ci docker pull "${MARIADB_REPO}:${MARIADB_TAG}" +run_quietly_if_ci docker pull "${FIREFOX_REPO}:${FIREFOX_TAG}" +run_quietly_if_ci docker pull "${WOPISERVER_REPO}:${WOPISERVER_TAG}" +run_quietly_if_ci docker pull "${SEAFILE_MC_REPO}:${SEAFILE_MC_TAG}" +run_quietly_if_ci docker pull "${KEYCLOAK_REPO}:${KEYCLOAK_TAG}" + +# ----------------------------------------------------------------------------------- +# Pull PonderSource-Specific Docker Images +# ----------------------------------------------------------------------------------- +run_quietly_if_ci echo "Pulling PonderSource-specific Docker images..." + +run_quietly_if_ci docker pull pondersource/revad:latest +run_quietly_if_ci docker pull pondersource/ocmstub:latest +run_quietly_if_ci docker pull pondersource/ocmstub:v1.0.0 + +# Nextcloud: Pull multiple versions of the Nextcloud Docker image. +run_quietly_if_ci docker pull pondersource/nextcloud-base:latest +nextcloud_versions=("latest" "v30.0.2" "v29.0.10" "v28.0.14" "v27.1.11") +for version in "${nextcloud_versions[@]}"; do + run_quietly_if_ci docker pull "pondersource/nextcloud:${version}" +done + +# ownCloud: Pull multiple versions of the ownCloud Docker image. +run_quietly_if_ci docker pull pondersource/owncloud-base:latest +owncloud_versions=("latest" "v10.15.0") +for version in "${owncloud_versions[@]}"; do + run_quietly_if_ci docker pull "pondersource/owncloud:${version}" +done + +# ----------------------------------------------------------------------------------- +# End of Docker Pulls +# ----------------------------------------------------------------------------------- +run_quietly_if_ci echo "Docker pull completed successfully." diff --git a/docker/push/all.sh b/docker/push/all.sh index 0a279fd0..2c7d31e9 100755 --- a/docker/push/all.sh +++ b/docker/push/all.sh @@ -1,28 +1,94 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -echo "Log in as pondersource" -docker login - -docker push pondersource/dev-stock-ocmstub:latest -docker push pondersource/dev-stock-ocmstub:v1.0.0 -docker push pondersource/dev-stock-revad:latest -docker push pondersource/dev-stock-php-base:latest -docker push pondersource/dev-stock-nextcloud:latest -docker push pondersource/dev-stock-nextcloud:v30.0.2 -docker push pondersource/dev-stock-nextcloud:v29.0.10 -docker push pondersource/dev-stock-nextcloud:v28.0.14 -docker push pondersource/dev-stock-nextcloud:v27.1.11 -# docker push pondersource/dev-stock-nextcloud-sunet -# docker push pondersource/dev-stock-simple-saml-php -docker push pondersource/dev-stock-nextcloud-solid:latest -docker push pondersource/dev-stock-nextcloud-sciencemesh:latest -docker push pondersource/dev-stock-owncloud:latest -docker push pondersource/dev-stock-owncloud-sciencemesh:latest -docker push pondersource/dev-stock-owncloud-surf-trashbin:latest -docker push pondersource/dev-stock-owncloud-token-based-access:latest -docker push pondersource/dev-stock-owncloud-opencloudmesh:latest -docker push pondersource/dev-stock-owncloud-federatedgroups:latest -docker push pondersource/dev-stock-owncloud-ocm-test-suite:latest +# ----------------------------------------------------------------------------------- +# Docker Push Script for PonderSource Development Images +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- +# This script pushes various Docker images for the PonderSource development environment, +# including both third-party and PonderSource-specific images. It ensures that necessary +# Docker images are pushed from specified repositories for continuous integration, +# development, and testing purposes. +# +# The script allows users to control the execution mode via a command-line argument +# (either 'dev' or 'ci'). In 'ci' mode, the output is suppressed to avoid clutter. +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Exit Immediately if a Command Fails +# ----------------------------------------------------------------------------------- +# Exit on error. If any command fails, the script stops execution to avoid issues +# with potentially broken states. The `pipefail` option ensures that if any command +# in a pipeline fails, the entire pipeline returns a non-zero exit status. +set -eo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default execution mode is 'dev', which shows full output. 'ci' mode suppresses output. +DEFAULT_SCRIPT_MODE="dev" + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout if in CI mode. +# Arguments: +# $@ - The command and its arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 # Suppress both stdout and stderr in CI mode. + else + "$@" # Run the command normally if not in CI mode. + fi +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + SCRIPT_MODE="${1:-$DEFAULT_SCRIPT_MODE}" # Default to 'dev' if no argument is provided. +} + +# ----------------------------------------------------------------------------------- +# Parse Command-Line Arguments +# ----------------------------------------------------------------------------------- +parse_arguments "$@" # Parse any arguments passed to the script. + +# Ensure successful login to Docker before pushing images. +echo "Logging in to Docker as pondersource..." +if ! docker login; then + echo "Docker login failed. Exiting." + exit 1 +fi + +# ----------------------------------------------------------------------------------- +# Push PonderSource-Specific Docker Images +# ----------------------------------------------------------------------------------- +run_quietly_if_ci echo "Pushing PonderSource-specific Docker images..." + +# Push the core PonderSource images. +run_quietly_if_ci docker push pondersource/revad:latest +run_quietly_if_ci docker push pondersource/ocmstub:latest +run_quietly_if_ci docker push pondersource/ocmstub:v1.0.0 + +# Nextcloud: push multiple versions of the Nextcloud Docker image. +run_quietly_if_ci docker push pondersource/nextcloud-base:latest +nextcloud_versions=("latest" "v30.0.2" "v29.0.10" "v28.0.14" "v27.1.11") +for version in "${nextcloud_versions[@]}"; do + run_quietly_if_ci docker push "pondersource/nextcloud:${version}" +done + +# ownCloud: push multiple versions of the ownCloud Docker image. +run_quietly_if_ci docker push pondersource/owncloud-base:latest +owncloud_versions=("latest" "v10.15.0") +for version in "${owncloud_versions[@]}"; do + run_quietly_if_ci docker push "pondersource/owncloud:${version}" +done + +# ----------------------------------------------------------------------------------- +# End of Docker Push +# ----------------------------------------------------------------------------------- +run_quietly_if_ci echo "Docker push completed successfully." From 301acffde7df48b99c8ac96c9d3df3ac0ac25697 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Mon, 6 Jan 2025 12:47:31 +0100 Subject: [PATCH 047/184] First attempt at ocmstub -> nextcloud share-with --- .../ocmstub-v1-to-nextcloud-v28.cy.js | 67 +++ .../share-with/ocmstub-nextcloud.sh | 452 ++++++++++++++++++ 2 files changed, 519 insertions(+) create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js create mode 100755 dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js new file mode 100644 index 00000000..fff2aa93 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js @@ -0,0 +1,67 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and Nextcloud v28. + * This suite verifies the ability to send and receive federated file shares between OcmStub and Nextcloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + acceptShareV28, + ensureFileExistsV28, +} from '../utils/nextcloud-v28'; + +describe('Native federated sharing functionality from OcmStub to Nextcloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const senderUsername = Cypress.env('OCMSTUB1_USERNAME') || 'tester'; + const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const expectedMessage = 'yes shareWith'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + fileName: 'Test share from stub', + owner: `${senderUsername}@${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`, + sender: `${senderUsername}@${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + + /** + * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + */ + it('should successfully send a federated share of a file from OcmStub 1.0 to OcmStub 1.0', () => { + // Step 1: Navigate to the federated share link on OcmStub 1.0 + // Remove trailing slash and leading https or http from recipientUrl + cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); + + // Step 2: Verify the confirmation message is displayed + cy.contains(expectedMessage, { timeout: 10000 }) + .should('be.visible') + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from Nextcloud v28 to Nextcloud v28', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV28(); + + // Step 3: Reload the page to ensure the shared file appears in the file list + cy.reload(true); + + // Step 4: Verify the shared file is visible + ensureFileExistsV28(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh new file mode 100755 index 00000000..39244a4d --- /dev/null +++ b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh @@ -0,0 +1,452 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test OcmStub to Nextcloud OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./ocmstub-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v1.0.0"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./ocmstub-nextcloud.sh v1.0.0 v28.0.14 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v1.0.0" +DEFAULT_EFSS_2_VERSION="v27.1.11" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} + + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://ocmstub1.docker (just click 'Log in')" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/ocmstub-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From fc824af2c672306237f84444de39641929ab0f66 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Mon, 6 Jan 2025 13:10:52 +0100 Subject: [PATCH 048/184] small corrections --- .../share-with/ocmstub-v1-to-nextcloud-v28.cy.js | 13 +------------ dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js index fff2aa93..de901e21 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js @@ -16,21 +16,10 @@ describe('Native federated sharing functionality from OcmStub to Nextcloud', () // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; - const senderUsername = Cypress.env('OCMSTUB1_USERNAME') || 'tester'; const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; const expectedMessage = 'yes shareWith'; - - // Expected details of the federated share - const expectedShareDetails = { - shareWith: `${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, - fileName: 'Test share from stub', - owner: `${senderUsername}@${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`, - sender: `${senderUsername}@${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`, - shareType: 'user', - resourceType: 'file', - protocol: 'webdav' - }; + const sharedFileName = '/Test share from stub'; /** * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. diff --git a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh index 39244a4d..495b026f 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh @@ -40,7 +40,7 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v1.0.0" -DEFAULT_EFSS_2_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v28.0.14" DEFAULT_SCRIPT_MODE="dev" DEFAULT_BROWSER_PLATFORM="electron" From bd985005ffdba847948db637bc76c9218082b746 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Mon, 6 Jan 2025 15:49:17 +0100 Subject: [PATCH 049/184] Attempt #162 --- docker/configs/nextcloud/apache.conf | 2 ++ docker/dockerfiles/nextcloud-base.Dockerfile | 2 +- docker/dockerfiles/nextcloud-sunet.Dockerfile | 2 +- docker/dockerfiles/owncloud-base.Dockerfile | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docker/configs/nextcloud/apache.conf b/docker/configs/nextcloud/apache.conf index 8eb259c5..94539b26 100644 --- a/docker/configs/nextcloud/apache.conf +++ b/docker/configs/nextcloud/apache.conf @@ -21,6 +21,8 @@ Header always set Strict-Transport-Security "max-age=63072000;" + ForensicLog forensic.log + SSLEngine on SSLCertificateFile "/tls/server.crt" SSLCertificateKeyFile "/tls/server.key" diff --git a/docker/dockerfiles/nextcloud-base.Dockerfile b/docker/dockerfiles/nextcloud-base.Dockerfile index e5ba52e9..55c6165a 100644 --- a/docker/dockerfiles/nextcloud-base.Dockerfile +++ b/docker/dockerfiles/nextcloud-base.Dockerfile @@ -144,7 +144,7 @@ RUN ln --symbolic --force /tls/*.crt /usr/local/share/ca-certificates; \ COPY ./configs/nextcloud/apache.conf /etc/apache2/sites-enabled/000-default.conf -RUN a2enmod headers rewrite remoteip ssl; \ +RUN a2enmod headers rewrite remoteip ssl log_forensic; \ { \ echo 'RemoteIPHeader X-Real-IP'; \ echo 'RemoteIPInternalProxy 10.0.0.0/8'; \ diff --git a/docker/dockerfiles/nextcloud-sunet.Dockerfile b/docker/dockerfiles/nextcloud-sunet.Dockerfile index 46667dd1..f1198e77 100644 --- a/docker/dockerfiles/nextcloud-sunet.Dockerfile +++ b/docker/dockerfiles/nextcloud-sunet.Dockerfile @@ -110,7 +110,7 @@ RUN docker-php-ext-enable \ redis # Enabling Modules -RUN a2enmod dir env headers mime rewrite setenvif deflate ssl +RUN a2enmod dir env headers mime rewrite setenvif deflate ssl log_forensic # Adjusting PHP settings RUN { \ diff --git a/docker/dockerfiles/owncloud-base.Dockerfile b/docker/dockerfiles/owncloud-base.Dockerfile index 10004180..8b77296c 100644 --- a/docker/dockerfiles/owncloud-base.Dockerfile +++ b/docker/dockerfiles/owncloud-base.Dockerfile @@ -137,7 +137,7 @@ RUN ln --symbolic --force /tls/*.crt /usr/local/share/ca-certificates; \ COPY ./configs/owncloud/apache.conf /etc/apache2/sites-enabled/000-default.conf -RUN a2enmod headers rewrite remoteip ssl; \ +RUN a2enmod headers rewrite remoteip ssl log_forensic; \ { \ echo 'RemoteIPHeader X-Real-IP'; \ echo 'RemoteIPInternalProxy 10.0.0.0/8'; \ From 135cc1e08d047a8cffd38137a8a917f3ac0db7ed Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Mon, 6 Jan 2025 16:57:19 +0100 Subject: [PATCH 050/184] install mod_security --- README.md | 9 +++++++++ docker/dockerfiles/nextcloud-base.Dockerfile | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/README.md b/README.md index c68792f1..eada09bc 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,15 @@ Use ci for automated pipelines with concise output. ### Cross-Platform Tests: For scenarios requiring two platforms (e.g., `share-with`, `invite-link`), specify both Platform 1 and Platform 2 along with their versions. +### Forensic Logs: +The Nextcloud images have [`mod_log_forensic`](https://httpd.apache.org/docs/2.4/mod/mod_log_forensic.html) and [`mod_security`](https://stackoverflow.com/a/47456612) +enabled, so you can try this for debugging: + +```bash +docker exec -it nextcloud1.docker tail -f /etc/apache2/forensic.log +docker exec -it nextcloud1.docker tail -f /var/log/apache2/modsec_audit.log +``` + # Debugging ## RD-SRAM diff --git a/docker/dockerfiles/nextcloud-base.Dockerfile b/docker/dockerfiles/nextcloud-base.Dockerfile index 55c6165a..54566a06 100644 --- a/docker/dockerfiles/nextcloud-base.Dockerfile +++ b/docker/dockerfiles/nextcloud-base.Dockerfile @@ -22,6 +22,7 @@ RUN set -ex; \ libldap-common \ ca-certificates \ libmagickcore-6.q16-6-extra \ + libapache2-mod-security2 \ ; \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ @@ -105,6 +106,14 @@ RUN set -ex; \ apt-get clean; \ rm -rf /var/lib/apt/lists/* +RUN { \ + echo 'SecRuleEngine On'; \ + echo 'SecAuditEngine On'; \ + echo 'SecAuditLog /var/log/apache2/modsec_audit.log'; \ + echo 'SecRequestBodyAccess on'; \ + echo 'SecAuditLogParts ABIJDFHZ'; \ + } > "/etc/modsecurity/modsecurity.conf"; + # set recommended PHP.ini settings # see https://docs.nextcloud.com/server/latest/admin_manual/installation/server_tuning.html#enable-php-opcache RUN { \ From 3b55b2ad5d0d9c41d737878695228815d574efd0 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 09:46:51 +0100 Subject: [PATCH 051/184] log response bodies --- docker/dockerfiles/nextcloud-base.Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/dockerfiles/nextcloud-base.Dockerfile b/docker/dockerfiles/nextcloud-base.Dockerfile index 54566a06..69ae93bd 100644 --- a/docker/dockerfiles/nextcloud-base.Dockerfile +++ b/docker/dockerfiles/nextcloud-base.Dockerfile @@ -111,7 +111,8 @@ RUN { \ echo 'SecAuditEngine On'; \ echo 'SecAuditLog /var/log/apache2/modsec_audit.log'; \ echo 'SecRequestBodyAccess on'; \ - echo 'SecAuditLogParts ABIJDFHZ'; \ + echo 'SecResponseBodyAccess On'; \ + echo 'SecAuditLogParts ABIJEFHZ'; \ } > "/etc/modsecurity/modsecurity.conf"; # set recommended PHP.ini settings From de03a6543572d370e2eb8135814e16e527998771 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 10:18:21 +0100 Subject: [PATCH 052/184] start ocmstub1.docker as sleeping so I can experiment with it --- dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh index 495b026f..eac1e835 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh @@ -272,7 +272,7 @@ function create_ocmstub() { run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ --name="ocmstub${number}.docker" \ -e HOST="ocmstub${number}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + "${image}:${tag}" sleep 30000 || error_exit "Failed to start EFSS container for ocmstub ${number}." # Wait for EFSS port to open run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 From 9ee0464688b0ec3aa7981a9a6ee57f7b6515e161 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 11:17:43 +0100 Subject: [PATCH 053/184] The mahdi/fix-grants branch was merged into the main branch --- dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh | 2 +- docker/dockerfiles/ocmstub.Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh index eac1e835..495b026f 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh @@ -272,7 +272,7 @@ function create_ocmstub() { run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ --name="ocmstub${number}.docker" \ -e HOST="ocmstub${number}" \ - "${image}:${tag}" sleep 30000 || error_exit "Failed to start EFSS container for ocmstub ${number}." + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." # Wait for EFSS port to open run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 diff --git a/docker/dockerfiles/ocmstub.Dockerfile b/docker/dockerfiles/ocmstub.Dockerfile index db50425a..d20a7536 100644 --- a/docker/dockerfiles/ocmstub.Dockerfile +++ b/docker/dockerfiles/ocmstub.Dockerfile @@ -33,7 +33,7 @@ RUN apt-get update && apt-get install --no-install-recommends --assume-yes \ # These allow customizing which repository and branch to clone at build time. # CACHEBUST is used to force rebuild steps when needed. ARG OCMSTUB_REPO=https://github.com/pondersource/ocm-stub -ARG OCMSTUB_BRANCH=mahdi/fix-grants +ARG OCMSTUB_BRANCH=main ARG CACHEBUST="default" # ---------------------------------------------------------------------------- From 9ff8db1f9fe56409e2e6275b8471e4867c714667 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 11:55:01 +0100 Subject: [PATCH 054/184] Fix ocmstub image tag --- .../e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js | 8 ++++---- cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js | 5 +++-- docker/build/ocm-test-suite.sh | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js index de901e21..e0592969 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js @@ -11,7 +11,7 @@ import { ensureFileExistsV28, } from '../utils/nextcloud-v28'; -describe('Native federated sharing functionality from OcmStub to Nextcloud', () => { +describe('Federated sharing functionality from OcmStub to Nextcloud', () => { // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; @@ -19,12 +19,12 @@ describe('Native federated sharing functionality from OcmStub to Nextcloud', () const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; const expectedMessage = 'yes shareWith'; - const sharedFileName = '/Test share from stub'; + const sharedFileName = '/from-stub.txt'; /** * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. */ - it('should successfully send a federated share of a file from OcmStub 1.0 to OcmStub 1.0', () => { + it('should successfully send a federated share of a file from OcmStub v1 to Nextcloud v28', () => { // Step 1: Navigate to the federated share link on OcmStub 1.0 // Remove trailing slash and leading https or http from recipientUrl cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); @@ -38,7 +38,7 @@ describe('Native federated sharing functionality from OcmStub to Nextcloud', () * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. * Validates that the recipient can successfully accept the share and view the shared file. */ - it('Receive federated share of a file from Nextcloud v28 to Nextcloud v28', () => { + it('Receive federated share of a file from OcmStub v1 to Nextcloud v28', () => { // Step 1: Log in to the recipient's Nextcloud instance cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js index 57a53d43..7bec8e6a 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js @@ -43,12 +43,13 @@ export function acceptShareV28() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') + // .first() .within(() => { // Locate the button row and click the primary button cy.get('div.oc-dialog-buttonrow') .find('button.primary') - .should('be.visible') - .click(); + // .should('be.visible') + .click({ force: true }); }); } diff --git a/docker/build/ocm-test-suite.sh b/docker/build/ocm-test-suite.sh index a1327781..0a359616 100755 --- a/docker/build/ocm-test-suite.sh +++ b/docker/build/ocm-test-suite.sh @@ -54,4 +54,4 @@ echo Building pondersource/dev-stock-owncloud-ocm-test-suite docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-ocm-test-suite.Dockerfile --tag pondersource/dev-stock-owncloud-ocm-test-suite:latest . echo Building pondersource/dev-stock-ocmstub -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/ocmstub.Dockerfile --tag pondersource/dev-stock-ocmstub:1.0 . +docker build --build-arg CACHEBUST="default" --file ./dockerfiles/ocmstub.Dockerfile --tag pondersource/dev-stock-ocmstub:v1.0.0 . From c514f36232118b7ee0c261f33a0defbaa260a12f Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 12:08:04 +0100 Subject: [PATCH 055/184] remove outdated build script to avoid confusion --- .../cypress/e2e/utils/nextcloud-v28.js | 9 +-- docker/build/ocm-test-suite.sh | 57 ------------------- 2 files changed, 5 insertions(+), 61 deletions(-) delete mode 100755 docker/build/ocm-test-suite.sh diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js index 7bec8e6a..443dc11b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js @@ -43,13 +43,14 @@ export function acceptShareV28() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - // .first() - .within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') + .each($div => { + cy.wrap($div).within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') .find('button.primary') // .should('be.visible') .click({ force: true }); + }) }); } diff --git a/docker/build/ocm-test-suite.sh b/docker/build/ocm-test-suite.sh deleted file mode 100755 index 0a359616..00000000 --- a/docker/build/ocm-test-suite.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} - -export DOCKER_BUILDKIT="${USE_BUILDKIT}" - -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . - -echo Building pondersource/dev-stock-revad -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/revad.Dockerfile --tag pondersource/dev-stock-revad:latest . - -echo Building pondersource/dev-stock-php-base -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v30.0.2" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.2 --tag pondersource/dev-stock-nextcloud:latest . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v29.0.10" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.10 . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v28.0.14" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.14 . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . - -echo Building pondersource/dev-stock-nextcloud-sciencemesh -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-sciencemesh.Dockerfile --tag pondersource/dev-stock-nextcloud-sciencemesh:latest . - -echo Building pondersource/dev-stock-owncloud -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud.Dockerfile --tag pondersource/dev-stock-owncloud:latest . - -echo Building pondersource/dev-stock-owncloud-sciencemesh -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-sciencemesh.Dockerfile --tag pondersource/dev-stock-owncloud-sciencemesh:latest . - -echo Building pondersource/dev-stock-owncloud-ocm-test-suite -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-ocm-test-suite.Dockerfile --tag pondersource/dev-stock-owncloud-ocm-test-suite:latest . - -echo Building pondersource/dev-stock-ocmstub -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/ocmstub.Dockerfile --tag pondersource/dev-stock-ocmstub:v1.0.0 . From 34dc9d955eb7b97716d0ad1b5ad88fc94fc959ba Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 12:19:31 +0100 Subject: [PATCH 056/184] Got it working --- .../cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js index e0592969..d191c0c6 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js @@ -19,7 +19,7 @@ describe('Federated sharing functionality from OcmStub to Nextcloud', () => { const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; const expectedMessage = 'yes shareWith'; - const sharedFileName = '/from-stub.txt'; + const sharedFileName = 'from-stub.txt'; /** * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. From 28acaeeafd8eb57f98e6b993c9e103d7b75d0cf8 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 14:00:59 +0100 Subject: [PATCH 057/184] ocmstub-v1-to-owncloud-v10 --- .../ocmstub-v1-to-owncloud-v10.cy.js | 57 +++ .../cypress/e2e/utils/owncloud.js | 16 +- .../share-with/ocmstub-owncloud.sh | 451 ++++++++++++++++++ 3 files changed, 517 insertions(+), 7 deletions(-) create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js create mode 100755 dev/ocm-test-suite/share-with/ocmstub-owncloud.sh diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js new file mode 100644 index 00000000..9b0c7933 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js @@ -0,0 +1,57 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and ownCloud v10. + * This suite verifies the ability to send and receive federated file shares between OcmStub and ownCloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + acceptShare, + ensureFileExists, + selectAppFromLeftSide, +} from '../utils/owncloud'; + +describe('Federated sharing functionality from OcmStub to ownCloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const recipientUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'relativity'; + const expectedMessage = 'yes shareWith'; + const sharedFileName = 'from-stub.txt'; + + /** + * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + */ + it('should successfully send a federated share of a file from OcmStub v1 to ownCloud v10', () => { + // Step 1: Navigate to the federated share link on OcmStub 1.0 + // Remove trailing slash and leading https or http from recipientUrl + cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); + + // Step 2: Verify the confirmation message is displayed + cy.contains(expectedMessage, { timeout: 10000 }) + .should('be.visible') + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's ownCloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from OcmStub v1 to ownCloud v10', () => { + // Step 1: Log in to the recipient's ownCloud instance + cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShare(); + + // Step 3: Navigate to the correct section + selectAppFromLeftSide('files'); + + // Step 4: Verify that the shared file is visible + ensureFileExists(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index 80ecb57b..1b8a7ca7 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -43,12 +43,14 @@ export function acceptShare() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') + .each($div => { + cy.wrap($div).within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') .find('button.primary') - .should('be.visible') - .click(); + // .should('be.visible') + .click({ force: true }); + }) }); } @@ -365,8 +367,8 @@ export function selectAppFromLeftSide(appId) { cy.get('div#app-navigation', { timeout: 10000 }) .should('be.visible') .find(`li[data-id="${appId}"]`) - .should('be.visible') - .click(); + // .should('be.visible') + .click({force: true}); } /** diff --git a/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh b/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh new file mode 100755 index 00000000..31000a78 --- /dev/null +++ b/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh @@ -0,0 +1,451 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test OcmStub to Nextcloud OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./ocmstub-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v1.0.0"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./ocmstub-owncloud.sh v1.0.0 v28.0.14 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v1.0.0" +DEFAULT_EFSS_2_VERSION="v10.15.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_owncloud +# Purpose: Create a ownCloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_owncloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaowncloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaowncloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="owncloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="owncloud${number}" \ + -e OWNCLOUD_HOST="owncloud${number}.docker" \ + -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ + -e OWNCLOUD_ADMIN_USER="${user}" \ + -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ + -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="mariaowncloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 1 "einstein" "relativity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://ocmstub1.docker (just click 'Log in')" + echo " https://owncloud1.docker (username: einstein, password: relativity)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/ocmstub-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From d39b71c136ac35c36f96563f7fbcc3d0ae178cf9 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 14:07:54 +0100 Subject: [PATCH 058/184] share-with ocmstub to NC27 --- .../ocmstub-v1-to-nextcloud-v27.cy.js | 56 +++++++++++++++++++ .../cypress/e2e/utils/nextcloud-v27.js | 12 ++-- 2 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js new file mode 100644 index 00000000..f8638a25 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js @@ -0,0 +1,56 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and Nextcloud v27. + * This suite verifies the ability to send and receive federated file shares between OcmStub and Nextcloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + acceptShareV27, + ensureFileExistsV27, +} from '../utils/nextcloud-v27'; + +describe('Federated sharing functionality from OcmStub to Nextcloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const expectedMessage = 'yes shareWith'; + const sharedFileName = 'from-stub.txt'; + + /** + * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + */ + it('should successfully send a federated share of a file from OcmStub v1 to Nextcloud v27', () => { + // Step 1: Navigate to the federated share link on OcmStub 1.0 + // Remove trailing slash and leading https or http from recipientUrl + cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); + + // Step 2: Verify the confirmation message is displayed + cy.contains(expectedMessage, { timeout: 10000 }) + .should('be.visible') + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from OcmStub v1 to Nextcloud v27', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV27(); + + // Step 3: Reload the page to ensure the shared file appears in the file list + cy.reload(true); + + // Step 4: Verify the shared file is visible + ensureFileExistsV27(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index 809151db..8b05f427 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -43,12 +43,14 @@ export function acceptShareV27() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') + .each($div => { + cy.wrap($div).within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') .find('button.primary') - .should('be.visible') - .click(); + // .should('be.visible') + .click({ force: true }); + }) }); } From 354e7ade2f86f2be9ec46856f57e73c9e4bec4fc Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 14:59:25 +0100 Subject: [PATCH 059/184] Add .sh and .cy.js for share-link nc27 -> os1 --- .../nextcloud-v27-to-ocmstub-v1.cy.js | 69 +++ .../share-link/nextcloud-ocmstub.sh | 452 ++++++++++++++++++ 2 files changed, 521 insertions(+) create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js create mode 100755 dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js new file mode 100644 index 00000000..e6331811 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -0,0 +1,69 @@ +import { + createShareLinkV27, + renameFileV27 +} from '../utils/nextcloud-v27' + +import { + generateShareAssertions, +} from '../utils/ocmstub-v1.js'; + +describe('Share link federated sharing functionality for Nextcloud', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'nc1-to-os1-share-link.txt'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}/`, + sender: `${senderUsername}@${senderUrl}/`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + + it('Send federated share from Nextcloud v27 to ocmstub v1', () => { + // send share from Nextcloud 1. + cy.loginNextcloud(senderUrl, senderUsername, senderPassword) + + renameFileV27(originalFileName, sharedFileName) + createShareLinkV27(sharedFileName).then( + (result) => { + cy.visit(result) + + cy.get('button[id="header-actions-toggle"]').click() + cy.get('button[id="save-external-share"]').click() + + cy.get('form[class="save-form"]').within(() => { + cy.get('input[id="remote_address"]').type(`${recipientUsername}@${recipientDomain}`) + cy.get('input[id="save-button-confirm"]').click() + }) + } + ) + }) + + it('Receive federated share from Nextcloud v27 to ocmstub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }) +}) diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh new file mode 100755 index 00000000..0997c4c7 --- /dev/null +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -0,0 +1,452 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-ocmstub.sh v28.0.14 v1.0.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v1.0.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} + + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://ocmstub1.docker (just click 'Log in')" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From c123fc1bc0e417f12bdc870712f981c6ee69cc77 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 15:07:38 +0100 Subject: [PATCH 060/184] share-link NC27 -> stub --- .../e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 7 ++++++- dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index e6331811..b75fa3a0 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -57,7 +57,12 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. const shareAssertions = generateShareAssertions(expectedShareDetails); - + + // FIXME: Remove this once https://github.com/nextcloud/server/issues/36340#issuecomment-2575333222 + // is resolved + shareAssertions['sharedBy'] = shareAssertions['sender']; + delete shareAssertions['sender']; + // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 0997c4c7..82c3aa5e 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ----------------------------------------------------------------------------------- -# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Script to Test Nextcloud to OcmStub OCM share-link flow tests. # Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- @@ -428,7 +428,7 @@ main() { "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ cypress run \ --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." # Revert Cypress configuration changes if [ "${BROWSER_PLATFORM}" != "electron" ]; then From d5bb722e0fbdffda55815d297180f517eeee8802 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 15:49:15 +0100 Subject: [PATCH 061/184] safer way to set recipientDomain --- .../cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index b75fa3a0..a372cc03 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -11,7 +11,7 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; - const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = 'ocmstub1.docker'; // recipientUrl.replace(/^https?:\/\/|\/$/g, ''); const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; From 23c99643cf7ae1a6e403be822889f2c95db53ed4 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:28:03 +0100 Subject: [PATCH 062/184] try to get share-link nc27 -> os1 working --- .../nextcloud-v27-to-ocmstub-v1.cy.js | 81 +-- .../share-link/nextcloud-ocmstub.sh | 612 +++++++----------- 2 files changed, 262 insertions(+), 431 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index a372cc03..de84ae9b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -1,40 +1,17 @@ import { createShareLinkV27, - renameFileV27 + renameFileV27, + navigationSwitchLeftSideV27, + selectAppFromLeftSideV27, } from '../utils/nextcloud-v27' -import { - generateShareAssertions, -} from '../utils/ocmstub-v1.js'; - describe('Share link federated sharing functionality for Nextcloud', () => { - // Shared variables to avoid repetition and improve maintainability - const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; - const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; - const recipientDomain = 'ocmstub1.docker'; // recipientUrl.replace(/^https?:\/\/|\/$/g, ''); - const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; - const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; - const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; - const originalFileName = 'welcome.txt'; - const sharedFileName = 'nc1-to-os1-share-link.txt'; - - // Expected details of the federated share - const expectedShareDetails = { - shareWith: `${recipientUsername}@${recipientUrl}`, - fileName: sharedFileName, - owner: `${senderUsername}@${senderUrl}/`, - sender: `${senderUsername}@${senderUrl}/`, - shareType: 'user', - resourceType: 'file', - protocol: 'webdav' - }; - - it('Send federated share from Nextcloud v27 to ocmstub v1', () => { + it('Send federated share from Nextcloud v27 to Nextcloud v27', () => { // send share from Nextcloud 1. - cy.loginNextcloud(senderUrl, senderUsername, senderPassword) + cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - renameFileV27(originalFileName, sharedFileName) - createShareLinkV27(sharedFileName).then( + renameFileV27('welcome.txt', 'nc1-to-nc2-share-link.txt') + createShareLinkV27('nc1-to-nc2-share-link.txt').then( (result) => { cy.visit(result) @@ -42,33 +19,31 @@ describe('Share link federated sharing functionality for Nextcloud', () => { cy.get('button[id="save-external-share"]').click() cy.get('form[class="save-form"]').within(() => { - cy.get('input[id="remote_address"]').type(`${recipientUsername}@${recipientDomain}`) + cy.get('input[id="remote_address"]').type('michiel@nextcloud2.docker') cy.get('input[id="save-button-confirm"]').click() }) } ) }) - it('Receive federated share from Nextcloud v27 to ocmstub v1', () => { - // Step 1: Log in to OcmStub - cy.loginOcmStub(recipientUrl); - - // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. - // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. - // Adjust these strings if the page format changes. - const shareAssertions = generateShareAssertions(expectedShareDetails); - - // FIXME: Remove this once https://github.com/nextcloud/server/issues/36340#issuecomment-2575333222 - // is resolved - shareAssertions['sharedBy'] = shareAssertions['sender']; - delete shareAssertions['sender']; - - // Step 2: Loop through all assertions and verify their presence on the page - // We use `cy.contains()` to search for the text anywhere on the page. - // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. - shareAssertions.forEach((assertion) => { - cy.contains(assertion, { timeout: 10000 }) - .should('be.visible'); - }); - }) + /** + * Test Case: Receiving a federated share on OcmStub from Nextcloud. + */ + it('Receive federated share of a file from from Nextcloud v27 to OcmStub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }); }) diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 82c3aa5e..2ad1d8b7 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -1,72 +1,131 @@ #!/usr/bin/env bash -# ----------------------------------------------------------------------------------- -# Script to Test Nextcloud to OcmStub OCM share-link flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Description: -# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms -# such as Nextcloud, OcmStub, using Cypress, and Docker containers. -# It supports both development and CI environments, with optional browser support. - -# Usage: -# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# @michielbdejong halt on error in docker init scripts. +set -e + +# find this scripts location. +SOURCE=${BASH_SOURCE[0]} +while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. + DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) + SOURCE=$(readlink "${SOURCE}") + # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. + [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" +done +DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) + +cd "${DIR}/../../.." || exit + +ENV_ROOT=$(pwd) +export ENV_ROOT=${ENV_ROOT} + +# nextcloud version: +# - v27.1.11 +# - v28.0.14 +EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} + +# nextcloud version: +# - v27.1.11 +# - v28.0.14 +EFSS_PLATFORM_2_VERSION=${2:-"v1.0.0"} + +# script mode: dev, ci. default is dev. +SCRIPT_MODE=${3:-"dev"} + +# browser platform: chrome, edge, firefox, electron. default is electron. +# only applies on SCRIPT_MODE=ci +BROWSER_PLATFORM=${4:-"electron"} + +function redirect_to_null_cmd() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} -# Arguments: -# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). -# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). -# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. -# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +function waitForPort () { + redirect_to_null_cmd echo waitForPort "${1} ${2}" + # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. + x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) + until [ "${x}" -ne 0 ] + do + redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" + sleep 1 + x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) + done + redirect_to_null_cmd echo "${1} port ${2} is open" +} -# Requirements: -# - Docker and required images must be installed. -# - Test scripts and configurations must be located in the expected directories. -# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. +function createEfss() { + local platform="${1}" + local number="${2}" + local user="${3}" + local password="${4}" + local init_script="${5}" + local tag="${6-latest}" + local image="${7}" + + if [[ -z "${image}" ]]; then + local image="pondersource/dev-stock-${platform}" + else + local image="pondersource/dev-stock-${platform}-${image}" + fi -# Example: -# ./nextcloud-ocmstub.sh v28.0.14 v1.0.0 ci electron + redirect_to_null_cmd echo "creating efss ${platform} ${number}" + + redirect_to_null_cmd docker run --detach --network=testnet \ + --name="maria${platform}${number}.docker" \ + -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ + mariadb:11.4.2 \ + --transaction-isolation=READ-COMMITTED \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed + + redirect_to_null_cmd docker run --detach --network=testnet \ + --name="${platform}${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="${platform}${number}" \ + -e DBHOST="maria${platform}${number}.docker" \ + -e USER="${user}" \ + -e PASS="${password}" \ + -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ + -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ + -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ + -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ + "${image}:${tag}" + + # wait for hostname port to be open. + waitForPort "maria${platform}${number}.docker" 3306 + waitForPort "${platform}${number}.docker" 443 + + # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) + docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 + docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 + docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 + docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 + docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 + + # run init script inside efss. + redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" + + redirect_to_null_cmd echo "" +} -# ----------------------------------------------------------------------------------- +# delete and create temp directory. +rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" -# Exit immediately if a command exits with a non-zero status, -# a variable is used but not defined, or a command in a pipeline fails -set -euo pipefail +# copy init files. +cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" -# ----------------------------------------------------------------------------------- -# Constants and Default Values -# ----------------------------------------------------------------------------------- +# auto clean before starting. +"${ENV_ROOT}/scripts/clean.sh" "no" -# Default versions -DEFAULT_EFSS_1_VERSION="v27.1.11" -DEFAULT_EFSS_2_VERSION="v1.0.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest +# make sure network exists. +docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 # ----------------------------------------------------------------------------------- -# Utility Functions +# Utility Functions Copied From dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- @@ -91,63 +150,6 @@ error_exit() { exit 1 } -# ----------------------------------------------------------------------------------- -# Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. -# ----------------------------------------------------------------------------------- -resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" -} - -# ----------------------------------------------------------------------------------- -# Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - # ----------------------------------------------------------------------------------- # Function: run_quietly_if_ci # Purpose: Run a command, suppressing stdout in CI mode. @@ -161,99 +163,6 @@ run_quietly_if_ci() { "$@" fi } - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - # ----------------------------------------------------------------------------------- # Function: create_ocmstub # Purpose: Create a OcmStub container. @@ -279,174 +188,121 @@ function create_ocmstub() { } -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi -} - -# ----------------------------------------------------------------------------------- -# Main Execution -# ----------------------------------------------------------------------------------- -main() { - # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi - - # Create EFSS containers - # # id # username # password # image # tag - create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" - create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" - - if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://ocmstub1.docker (just click 'Log in')" - - else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi - fi -} +################# +### Nextcloud ### +################# + +# syntax: +# createEfss platform number username password image. +# +# +# platform: owncloud, nextcloud. +# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. +# username: username for sign in into efss. +# password: password for sign in into efss. +# init script: script for initializing efss. +# tag: tag for the image, use latest if not sure. +# image: which image variation to use for container. + +# Nextclouds. +createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" +create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + +# disable cypress editing javascript files. it would make adding share to your own efss fail. +sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + +if [ "${SCRIPT_MODE}" = "dev" ]; then + ############### + ### Firefox ### + ############### + + docker run --detach --network=testnet \ + --name=firefox \ + -p 5800:5800 \ + --shm-size 2g \ + -e USER_ID="${UID}" \ + -e GROUP_ID="${UID}" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + jlesage/firefox:latest \ + >/dev/null 2>&1 + + ################## + ### VNC Server ### + ################## + + # remove previous x11 unix socket file, avoid any problems while mounting new one. + sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" + + # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, + # NOTE: please do not commit any change related to resolution. + docker run --detach --network=testnet \ + --name=vnc-server \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ + theasp/novnc:latest + + ############### + ### Cypress ### + ############### + + # create cypress and attach its display to the VNC server container. + # this way you can view inside cypress container through vnc server. + docker run --detach --network=testnet \ + --name="cypress.docker" \ + -e DISPLAY=vnc-server:0.0 \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + cypress/included:13.13.1 \ + open --project . + + # print instructions. + clear + echo "Now browse to :" + echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "Embedded Firefox -> http://localhost:5800" + echo "" + echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" + echo "https://nextcloud1.docker -> username: einstein password: relativity" + echo "https://nextcloud2.docker -> username: michiel password: dejong" +else + # only record when testing on electron. + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi + ################## + ### Cypress CI ### + ################## + + # extract version up until first dot . , example: v27.1.17 becomes v27 + P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" + P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" + + # for some reason this test fails on ci! so lets sleep a bit. + sleep 60 + + # run Cypress test suite headlessly and with the defined browser. + docker run --network=testnet \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + cypress/included:13.13.1 cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" + + # revert config file back to normal. + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi -# ----------------------------------------------------------------------------------- -# Execute the main function with passed arguments -# ----------------------------------------------------------------------------------- -main "$@" + # auto clean after running tests in ci mode. do not clear terminal. + "${ENV_ROOT}/scripts/clean.sh" "no" +fi From 3521199396e2ac24b00ebd6714aa20d57024c3cb Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:29:36 +0100 Subject: [PATCH 063/184] specify missing DOCKER_NETWORK env var --- dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 2ad1d8b7..4e2700f1 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -128,6 +128,9 @@ docker network inspect testnet >/dev/null 2>&1 || docker network create testnet # Utility Functions Copied From dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh # ----------------------------------------------------------------------------------- +# Docker network name +DOCKER_NETWORK="testnet" + # ----------------------------------------------------------------------------------- # Function: print_error # Purpose: Print an error message to stderr. From 3c8bc5b17c1f7b3653c7088e464a6b42a089b839 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:36:16 +0100 Subject: [PATCH 064/184] Update script for dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh --- .../share-link/nextcloud-nextcloud.sh | 648 +++++++++++------- 1 file changed, 417 insertions(+), 231 deletions(-) diff --git a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh index ddc2c56f..dfdad6df 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh @@ -1,41 +1,160 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_2_VERSION=${2:-"v27.1.11"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to Nextcloud OCM share-link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-nextcloud.sh v28.0.14 v27.1.11 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v27.1.11" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { if [ "${SCRIPT_MODE}" = "ci" ]; then "$@" >/dev/null 2>&1 else @@ -43,202 +162,269 @@ function redirect_to_null_cmd() { fi } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi } -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + + # disable cypress editing javascript files. it would make adding share to your own efss fail. + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://nextcloud2.docker (username: michiel, password: dejong)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# Nextclouds. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" -createEfss nextcloud 2 michiel dejong nextcloud.sh "${EFSS_PLATFORM_2_VERSION}" - -# disable cypress editing javascript files. it would make adding share to your own efss fail. -sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://nextcloud2.docker -> username: michiel password: dejong" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # for some reason this test fails on ci! so lets sleep a bit. - sleep 60 - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From 470d547c0ed203520ae446c0ce39fe0eb9705741 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:44:24 +0100 Subject: [PATCH 065/184] share-link NC27 -> OS1, the new way --- cypress/ocm-test-suite/cypress.config.js | 2 +- .../nextcloud-v27-to-ocmstub-v1.cy.js | 4 + .../share-link/nextcloud-ocmstub.sh | 612 +++++++++++------- 3 files changed, 383 insertions(+), 235 deletions(-) diff --git a/cypress/ocm-test-suite/cypress.config.js b/cypress/ocm-test-suite/cypress.config.js index ebcf57c7..b65a4119 100644 --- a/cypress/ocm-test-suite/cypress.config.js +++ b/cypress/ocm-test-suite/cypress.config.js @@ -7,5 +7,5 @@ module.exports = defineConfig({ chromeWebSecurity: false, video: true, videoCompression: true, - modifyObstructiveCode: true, + modifyObstructiveCode: false, }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index de84ae9b..def55e97 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -38,6 +38,10 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // Adjust these strings if the page format changes. const shareAssertions = generateShareAssertions(expectedShareDetails); + // work around https://github.com/nextcloud/server/issues/36340 + shareAssertions['sharedBy'] = shareAssertions['sender']; + delete shareAssertions['sender']; + // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 4e2700f1..ad3f2064 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -1,136 +1,74 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_2_VERSION=${2:-"v1.0.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi +# Usage: +# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" +# Example: +# ./nextcloud-ocmstub.sh v28.0.14 v1.0.0 ci electron -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" +# ----------------------------------------------------------------------------------- -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail # ----------------------------------------------------------------------------------- -# Utility Functions Copied From dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh +# Constants and Default Values # ----------------------------------------------------------------------------------- +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v1.0.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + # Docker network name DOCKER_NETWORK="testnet" +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + # ----------------------------------------------------------------------------------- # Function: print_error # Purpose: Print an error message to stderr. @@ -153,6 +91,63 @@ error_exit() { exit 1 } +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + # ----------------------------------------------------------------------------------- # Function: run_quietly_if_ci # Purpose: Run a command, suppressing stdout in CI mode. @@ -166,6 +161,99 @@ run_quietly_if_ci() { "$@" fi } + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + # ----------------------------------------------------------------------------------- # Function: create_ocmstub # Purpose: Create a OcmStub container. @@ -191,121 +279,177 @@ function create_ocmstub() { } -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# Nextclouds. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" -create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" - -# disable cypress editing javascript files. it would make adding share to your own efss fail. -sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://nextcloud2.docker -> username: michiel password: dejong" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # for some reason this test fails on ci! so lets sleep a bit. - sleep 60 - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + + # disable cypress editing javascript files. it would make adding share to your own efss fail. + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://ocmstub1.docker (just click 'Log in')" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From a413c72384d0ebe4fabed5e00af861a9b1378d31 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:46:41 +0100 Subject: [PATCH 066/184] Update share-with -> share-link in copied file --- dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index ad3f2064..b3db9e7d 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ----------------------------------------------------------------------------------- -# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Script to Test Nextcloud to OcmStub OCM share-link flow tests. # Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- @@ -431,7 +431,7 @@ main() { "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ cypress run \ --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." # Revert Cypress configuration changes if [ "${BROWSER_PLATFORM}" != "electron" ]; then From 44b4c6e9f3dc5dd7db96d5c9d587635899e7c4d8 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:58:17 +0100 Subject: [PATCH 067/184] Use variables --- .../nextcloud-v27-to-ocmstub-v1.cy.js | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index def55e97..5bd9af2e 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -1,17 +1,40 @@ import { createShareLinkV27, renameFileV27, - navigationSwitchLeftSideV27, - selectAppFromLeftSideV27, -} from '../utils/nextcloud-v27' +} from '../utils/nextcloud-v27'; + +import { + generateShareAssertions, +} from '../utils/ocmstub-v1.js'; describe('Share link federated sharing functionality for Nextcloud', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'nc1-to-os1-share.txt'; + const sharedWith = 'michiel@ocmstub1.docker'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}/`, + sender: `${senderUsername}@${senderUrl}/`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + it('Send federated share from Nextcloud v27 to Nextcloud v27', () => { // send share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') + cy.loginNextcloud(senderUrl, senderUsername, senderPassword) - renameFileV27('welcome.txt', 'nc1-to-nc2-share-link.txt') - createShareLinkV27('nc1-to-nc2-share-link.txt').then( + renameFileV27(originalFileName, sharedFileName) + createShareLinkV27(sharedFileName).then( (result) => { cy.visit(result) @@ -19,7 +42,7 @@ describe('Share link federated sharing functionality for Nextcloud', () => { cy.get('button[id="save-external-share"]').click() cy.get('form[class="save-form"]').within(() => { - cy.get('input[id="remote_address"]').type('michiel@nextcloud2.docker') + cy.get('input[id="remote_address"]').type(sharedWith) cy.get('input[id="save-button-confirm"]').click() }) } From d603a535bc8cc7afe241c9c20433bb04576843c4 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 17:01:50 +0100 Subject: [PATCH 068/184] allowNcDivergence --- cypress/ocm-test-suite/cypress.config.js | 2 +- .../e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 6 +----- cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cypress/ocm-test-suite/cypress.config.js b/cypress/ocm-test-suite/cypress.config.js index b65a4119..ebcf57c7 100644 --- a/cypress/ocm-test-suite/cypress.config.js +++ b/cypress/ocm-test-suite/cypress.config.js @@ -7,5 +7,5 @@ module.exports = defineConfig({ chromeWebSecurity: false, video: true, videoCompression: true, - modifyObstructiveCode: false, + modifyObstructiveCode: true, }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index 5bd9af2e..5672617c 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -59,11 +59,7 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. - const shareAssertions = generateShareAssertions(expectedShareDetails); - - // work around https://github.com/nextcloud/server/issues/36340 - shareAssertions['sharedBy'] = shareAssertions['sender']; - delete shareAssertions['sender']; + const shareAssertions = generateShareAssertions(expectedShareDetails, true); // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js index 87bf94a3..316d2d07 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js @@ -39,7 +39,7 @@ * // The returned array can then be used with `cy.contains()` calls in Cypress tests: * // shareAssertions.forEach(assertion => cy.contains(assertion).should('be.visible')); */ -export function generateShareAssertions(expectedDetails) { +export function generateShareAssertions(expectedDetails, allowNcDivergence = false) { // Required fields that must be present and non-empty strings const requiredFields = [ 'shareWith', @@ -73,7 +73,7 @@ export function generateShareAssertions(expectedDetails) { `"providerId":`, `"shareType": "${expectedDetails.shareType}"`, `"owner": "${expectedDetails.owner}"`, - `"sender": "${expectedDetails.sender}"`, + (allowNcDivergence? `"sharedBy": "${expectedDetails.sender}"` : `"sender": "${expectedDetails.sender}"`), `"resourceType": "${expectedDetails.resourceType}"`, // For protocol, we know 'name' but 'sharedSecret' may vary. // We assert on part of the structure to ensure the protocol block is present. From 6915c66b8760a9ec2e17e04530dff39c823afacd Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 08:56:28 +0000 Subject: [PATCH 069/184] fix: JSONArgsRecommended by specifying a SHELL before CMD --- docker/dockerfiles/nextcloud.Dockerfile | 1 + docker/dockerfiles/owncloud.Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/dockerfiles/nextcloud.Dockerfile b/docker/dockerfiles/nextcloud.Dockerfile index 70c66711..e48d31f5 100644 --- a/docker/dockerfiles/nextcloud.Dockerfile +++ b/docker/dockerfiles/nextcloud.Dockerfile @@ -35,5 +35,6 @@ COPY ./scripts/nextcloud/*.sh / COPY ./scripts/nextcloud/upgrade.exclude / COPY ./configs/nextcloud/* /usr/src/nextcloud/config/ +SHELL ["/bin/bash", "-c"] ENTRYPOINT ["/entrypoint.sh"] CMD apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow /var/www/html/data/nextcloud.log diff --git a/docker/dockerfiles/owncloud.Dockerfile b/docker/dockerfiles/owncloud.Dockerfile index cfc14ffc..4adb6db2 100644 --- a/docker/dockerfiles/owncloud.Dockerfile +++ b/docker/dockerfiles/owncloud.Dockerfile @@ -39,5 +39,6 @@ COPY ./scripts/owncloud/*.sh / COPY ./scripts/owncloud/upgrade.exclude / COPY ./configs/owncloud/* /usr/src/owncloud/config/ +SHELL ["/bin/bash", "-c"] ENTRYPOINT ["/entrypoint.sh"] CMD apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow /var/www/html/data/owncloud.log From 479df714256d6ac64be2e5ac4859e51edf1f9cb4 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 09:16:32 +0000 Subject: [PATCH 070/184] add: authors in header --- .../share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 16 ++++++++++++---- .../share-link/nextcloud-nextcloud.sh | 4 +++- .../share-link/nextcloud-ocmstub.sh | 4 +++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index 5672617c..533a609e 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -1,3 +1,11 @@ +/** + * @fileoverview + * Cypress test suite for testing federated sharing functionality via share-link flow in Nextcloud v27 and OcmStub v1. + * + * @author Michiel B. de Jong + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { createShareLinkV27, renameFileV27, @@ -7,7 +15,7 @@ import { generateShareAssertions, } from '../utils/ocmstub-v1.js'; -describe('Share link federated sharing functionality for Nextcloud', () => { +describe('Share link federated sharing functionality for Nextcloud to OcmStub', () => { // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; @@ -29,7 +37,7 @@ describe('Share link federated sharing functionality for Nextcloud', () => { protocol: 'webdav' }; - it('Send federated share from Nextcloud v27 to Nextcloud v27', () => { + it('Send federated share from Nextcloud v27 to OcmStub v1', () => { // send share from Nextcloud 1. cy.loginNextcloud(senderUrl, senderUsername, senderPassword) @@ -56,12 +64,12 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // Step 1: Log in to OcmStub cy.loginOcmStub(recipientUrl); - // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // Step 2: Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. const shareAssertions = generateShareAssertions(expectedShareDetails, true); - // Step 2: Loop through all assertions and verify their presence on the page + // Step 3: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. shareAssertions.forEach((assertion) => { diff --git a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh index dfdad6df..010e5875 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh @@ -2,7 +2,9 @@ # ----------------------------------------------------------------------------------- # Script to Test Nextcloud to Nextcloud OCM share-link flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index b3db9e7d..3874ba4b 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -2,7 +2,9 @@ # ----------------------------------------------------------------------------------- # Script to Test Nextcloud to OcmStub OCM share-link flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- From 6464bfc46de1758af76d63490e160bbe9d1e508e Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 09:17:02 +0000 Subject: [PATCH 071/184] add: argument in docstrings and TODO comment for future --- cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js index 316d2d07..ae938e40 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js @@ -18,6 +18,7 @@ * @param {string} expectedDetails.sender - The sender of the shared resource (e.g., "einstein@nextcloud.com"). * @param {string} expectedDetails.resourceType - The type of the shared resource ("file", "folder", etc.). * @param {string} expectedDetails.protocol - The protocol used for the share (e.g., "webdav"). + * @param {boolean} isNextcloud - Whether the instance being checked is nectcloud or not. * @returns {string[]} - An array of strings representing expected lines of text that should appear in the UI. * * @throws {Error} Throws an error if any required field in expectedDetails is missing or not a string. @@ -35,11 +36,11 @@ * }; * * // Generate the share assertions - * const shareAssertions = generateShareAssertions(expectedDetails); + * const shareAssertions = generateShareAssertions(expectedDetails, true); * // The returned array can then be used with `cy.contains()` calls in Cypress tests: * // shareAssertions.forEach(assertion => cy.contains(assertion).should('be.visible')); */ -export function generateShareAssertions(expectedDetails, allowNcDivergence = false) { +export function generateShareAssertions(expectedDetails, isNextcloud = false) { // Required fields that must be present and non-empty strings const requiredFields = [ 'shareWith', @@ -73,7 +74,10 @@ export function generateShareAssertions(expectedDetails, allowNcDivergence = fal `"providerId":`, `"shareType": "${expectedDetails.shareType}"`, `"owner": "${expectedDetails.owner}"`, - (allowNcDivergence? `"sharedBy": "${expectedDetails.sender}"` : `"sender": "${expectedDetails.sender}"`), + // Nextcloud is not following OCM specification at this moment, see: https://github.com/nextcloud/server/issues/36340#issuecomment-2575333222 + // this should be fixed via https://github.com/nextcloud/server/pull/50069 + // TODO @MahdiBaghbani: rename this to isLegacyNextcloud once the PR is merged and available in a new Nextcloud release. + (isNextcloud? `"sharedBy": "${expectedDetails.sender}"` : `"sender": "${expectedDetails.sender}"`), `"resourceType": "${expectedDetails.resourceType}"`, // For protocol, we know 'name' but 'sharedSecret' may vary. // We assert on part of the structure to ensure the protocol block is present. From ffc772fc61e13d6c06feb3d7b8295bf09f67acf0 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 09:19:21 +0000 Subject: [PATCH 072/184] add: GtiHub actions test runner --- .github/workflows/share-link-nc-v27-os-v1.yml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/share-link-nc-v27-os-v1.yml diff --git a/.github/workflows/share-link-nc-v27-os-v1.yml b/.github/workflows/share-link-nc-v27-os-v1.yml new file mode 100644 index 00000000..2ad331fa --- /dev/null +++ b/.github/workflows/share-link-nc-v27-os-v1.yml @@ -0,0 +1,66 @@ +name: OCM Test Share Link NC v27.1.11 to OcmStub v1.0.0 + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the specified branch and files. + push: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + pull_request: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + # Allows you to run this workflow manually from the Actions tab. + workflow_dispatch: + +jobs: + share-link: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + sender: [ + { + platform: nextcloud, + version: v27.1.11 + }, + ] + receiver: [ + { + platform: ocmstub, + version: "v1.0.0" + }, + ] + + # The OS to run tests on, (I believe for OCM testing OS is really not that important). + runs-on: ubuntu-24.04 + + # Steps represent a sequence of tasks that will be executed as part of the job. + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. + - name: Checkout. + uses: actions/checkout@v4 + + - name: Pull images. + shell: bash + run: | + ./docker/pull/ocm-test-suite/${{ matrix.sender.platform }}.sh ${{ matrix.sender.version }} + ./docker/pull/ocm-test-suite/${{ matrix.receiver.platform }}.sh ${{ matrix.receiver.version }} + + - name: Run tests. + shell: bash + run: ./dev/ocm-test-suite.sh share-link ${{ matrix.sender.platform }} ${{ matrix.sender.version }} ci electron ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + + - name: Upload Cypress video artifacts. + uses: actions/upload-artifact@v4 + if: always() + with: + name: share-link from ${{ matrix.sender.platform }} ${{ matrix.sender.version }} to ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + path: ./cypress/ocm-test-suite/cypress/videos From 7970e48c75b7880759d28b581f1008aae9c756be Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 11:38:57 +0000 Subject: [PATCH 073/184] modify: use first instead of each --- .../ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index 8b05f427..b19cd537 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -43,14 +43,13 @@ export function acceptShareV27() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .each($div => { - cy.wrap($div).within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') + .first() + .within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') .find('button.primary') - // .should('be.visible') + .should('exist') .click({ force: true }); - }) }); } From d179851f6806e4d8955ab5142249df23762d0f71 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 11:44:40 +0000 Subject: [PATCH 074/184] add: authors --- .../cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js index f8638a25..ac2b66f7 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js @@ -3,6 +3,7 @@ * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and Nextcloud v27. * This suite verifies the ability to send and receive federated file shares between OcmStub and Nextcloud. * + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ From 7860986f1954727cfc79619518b8bd001c7a0954 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 12:00:20 +0000 Subject: [PATCH 075/184] add: GtiHub actions workflow --- .github/workflows/share-with-os-v1-nc-v27.yml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/share-with-os-v1-nc-v27.yml diff --git a/.github/workflows/share-with-os-v1-nc-v27.yml b/.github/workflows/share-with-os-v1-nc-v27.yml new file mode 100644 index 00000000..d4c8f1c0 --- /dev/null +++ b/.github/workflows/share-with-os-v1-nc-v27.yml @@ -0,0 +1,66 @@ +name: OCM Test Share With OcmStub v1.0.0 to NC v27.1.11 + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the specified branch and files. + push: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + pull_request: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + # Allows you to run this workflow manually from the Actions tab. + workflow_dispatch: + +jobs: + share-with: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + sender: [ + { + platform: ocmstub, + version: "v1.0.0" + }, + ] + receiver: [ + { + platform: nextcloud, + version: v27.1.11 + }, + ] + + # The OS to run tests on, (I believe for OCM testing OS is really not that important). + runs-on: ubuntu-24.04 + + # Steps represent a sequence of tasks that will be executed as part of the job. + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. + - name: Checkout. + uses: actions/checkout@v4 + + - name: Pull images. + shell: bash + run: | + ./docker/pull/ocm-test-suite/${{ matrix.sender.platform }}.sh ${{ matrix.sender.version }} + ./docker/pull/ocm-test-suite/${{ matrix.receiver.platform }}.sh ${{ matrix.receiver.version }} + + - name: Run tests. + shell: bash + run: ./dev/ocm-test-suite.sh share-with ${{ matrix.sender.platform }} ${{ matrix.sender.version }} ci electron ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + + - name: Upload Cypress video artifacts. + uses: actions/upload-artifact@v4 + if: always() + with: + name: share-with from ${{ matrix.sender.platform }} ${{ matrix.sender.version }} to ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + path: ./cypress/ocm-test-suite/cypress/videos From ba82ee57aabf0f1fdb903d947fca0297e481e4a7 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 14:00:59 +0100 Subject: [PATCH 076/184] ocmstub-v1-to-owncloud-v10 --- .../ocmstub-v1-to-owncloud-v10.cy.js | 57 +++ .../cypress/e2e/utils/owncloud.js | 16 +- .../share-with/ocmstub-owncloud.sh | 451 ++++++++++++++++++ 3 files changed, 517 insertions(+), 7 deletions(-) create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js create mode 100755 dev/ocm-test-suite/share-with/ocmstub-owncloud.sh diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js new file mode 100644 index 00000000..9b0c7933 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js @@ -0,0 +1,57 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and ownCloud v10. + * This suite verifies the ability to send and receive federated file shares between OcmStub and ownCloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + acceptShare, + ensureFileExists, + selectAppFromLeftSide, +} from '../utils/owncloud'; + +describe('Federated sharing functionality from OcmStub to ownCloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const recipientUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'relativity'; + const expectedMessage = 'yes shareWith'; + const sharedFileName = 'from-stub.txt'; + + /** + * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + */ + it('should successfully send a federated share of a file from OcmStub v1 to ownCloud v10', () => { + // Step 1: Navigate to the federated share link on OcmStub 1.0 + // Remove trailing slash and leading https or http from recipientUrl + cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); + + // Step 2: Verify the confirmation message is displayed + cy.contains(expectedMessage, { timeout: 10000 }) + .should('be.visible') + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's ownCloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from OcmStub v1 to ownCloud v10', () => { + // Step 1: Log in to the recipient's ownCloud instance + cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShare(); + + // Step 3: Navigate to the correct section + selectAppFromLeftSide('files'); + + // Step 4: Verify that the shared file is visible + ensureFileExists(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index 80ecb57b..1b8a7ca7 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -43,12 +43,14 @@ export function acceptShare() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') + .each($div => { + cy.wrap($div).within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') .find('button.primary') - .should('be.visible') - .click(); + // .should('be.visible') + .click({ force: true }); + }) }); } @@ -365,8 +367,8 @@ export function selectAppFromLeftSide(appId) { cy.get('div#app-navigation', { timeout: 10000 }) .should('be.visible') .find(`li[data-id="${appId}"]`) - .should('be.visible') - .click(); + // .should('be.visible') + .click({force: true}); } /** diff --git a/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh b/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh new file mode 100755 index 00000000..31000a78 --- /dev/null +++ b/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh @@ -0,0 +1,451 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test OcmStub to Nextcloud OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./ocmstub-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v1.0.0"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./ocmstub-owncloud.sh v1.0.0 v28.0.14 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v1.0.0" +DEFAULT_EFSS_2_VERSION="v10.15.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_owncloud +# Purpose: Create a ownCloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_owncloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaowncloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaowncloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="owncloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="owncloud${number}" \ + -e OWNCLOUD_HOST="owncloud${number}.docker" \ + -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ + -e OWNCLOUD_ADMIN_USER="${user}" \ + -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ + -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="mariaowncloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 1 "einstein" "relativity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://ocmstub1.docker (just click 'Log in')" + echo " https://owncloud1.docker (username: einstein, password: relativity)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/ocmstub-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From bc9c3438207b461b71a8839e06f89fa3997676e9 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 14:07:54 +0100 Subject: [PATCH 077/184] share-with ocmstub to NC27 --- .../ocmstub-v1-to-nextcloud-v27.cy.js | 56 +++++++++++++++++++ .../cypress/e2e/utils/nextcloud-v27.js | 12 ++-- 2 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js new file mode 100644 index 00000000..f8638a25 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js @@ -0,0 +1,56 @@ +/** + * @fileoverview + * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and Nextcloud v27. + * This suite verifies the ability to send and receive federated file shares between OcmStub and Nextcloud. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +import { + acceptShareV27, + ensureFileExistsV27, +} from '../utils/nextcloud-v27'; + +describe('Federated sharing functionality from OcmStub to Nextcloud', () => { + + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const expectedMessage = 'yes shareWith'; + const sharedFileName = 'from-stub.txt'; + + /** + * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + */ + it('should successfully send a federated share of a file from OcmStub v1 to Nextcloud v27', () => { + // Step 1: Navigate to the federated share link on OcmStub 1.0 + // Remove trailing slash and leading https or http from recipientUrl + cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); + + // Step 2: Verify the confirmation message is displayed + cy.contains(expectedMessage, { timeout: 10000 }) + .should('be.visible') + }); + + /** + * Test Case: Receiving and accepting a federated share on the recipient's Nextcloud instance. + * Validates that the recipient can successfully accept the share and view the shared file. + */ + it('Receive federated share of a file from OcmStub v1 to Nextcloud v27', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV27(); + + // Step 3: Reload the page to ensure the shared file appears in the file list + cy.reload(true); + + // Step 4: Verify the shared file is visible + ensureFileExistsV27(sharedFileName); + + // TODO @MahdiBaghbani: Download or open the file to verify content (if required) + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index 809151db..8b05f427 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -43,12 +43,14 @@ export function acceptShareV27() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') + .each($div => { + cy.wrap($div).within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') .find('button.primary') - .should('be.visible') - .click(); + // .should('be.visible') + .click({ force: true }); + }) }); } From 03f7da5c6796fe878ec911bac4da72a0034e60a2 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 14:59:25 +0100 Subject: [PATCH 078/184] Add .sh and .cy.js for share-link nc27 -> os1 --- .../nextcloud-v27-to-ocmstub-v1.cy.js | 69 +++ .../share-link/nextcloud-ocmstub.sh | 452 ++++++++++++++++++ 2 files changed, 521 insertions(+) create mode 100644 cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js create mode 100755 dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js new file mode 100644 index 00000000..e6331811 --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -0,0 +1,69 @@ +import { + createShareLinkV27, + renameFileV27 +} from '../utils/nextcloud-v27' + +import { + generateShareAssertions, +} from '../utils/ocmstub-v1.js'; + +describe('Share link federated sharing functionality for Nextcloud', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'nc1-to-os1-share-link.txt'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}/`, + sender: `${senderUsername}@${senderUrl}/`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + + it('Send federated share from Nextcloud v27 to ocmstub v1', () => { + // send share from Nextcloud 1. + cy.loginNextcloud(senderUrl, senderUsername, senderPassword) + + renameFileV27(originalFileName, sharedFileName) + createShareLinkV27(sharedFileName).then( + (result) => { + cy.visit(result) + + cy.get('button[id="header-actions-toggle"]').click() + cy.get('button[id="save-external-share"]').click() + + cy.get('form[class="save-form"]').within(() => { + cy.get('input[id="remote_address"]').type(`${recipientUsername}@${recipientDomain}`) + cy.get('input[id="save-button-confirm"]').click() + }) + } + ) + }) + + it('Receive federated share from Nextcloud v27 to ocmstub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }) +}) diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh new file mode 100755 index 00000000..0997c4c7 --- /dev/null +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -0,0 +1,452 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-ocmstub.sh v28.0.14 v1.0.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v1.0.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} + + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://ocmstub1.docker (just click 'Log in')" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From 88f548d528aa4cb3f36fbd3ce3dad7c47e4fe9f7 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 15:07:38 +0100 Subject: [PATCH 079/184] share-link NC27 -> stub --- .../e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 7 ++++++- dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index e6331811..b75fa3a0 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -57,7 +57,12 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. const shareAssertions = generateShareAssertions(expectedShareDetails); - + + // FIXME: Remove this once https://github.com/nextcloud/server/issues/36340#issuecomment-2575333222 + // is resolved + shareAssertions['sharedBy'] = shareAssertions['sender']; + delete shareAssertions['sender']; + // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 0997c4c7..82c3aa5e 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ----------------------------------------------------------------------------------- -# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Script to Test Nextcloud to OcmStub OCM share-link flow tests. # Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- @@ -428,7 +428,7 @@ main() { "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ cypress run \ --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." # Revert Cypress configuration changes if [ "${BROWSER_PLATFORM}" != "electron" ]; then From f675646573416f2e6f43ab7b5f4f96070b9e26fa Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 15:49:15 +0100 Subject: [PATCH 080/184] safer way to set recipientDomain --- .../cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index b75fa3a0..a372cc03 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -11,7 +11,7 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; - const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = 'ocmstub1.docker'; // recipientUrl.replace(/^https?:\/\/|\/$/g, ''); const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; From 5903fdf4bd495e99eb97edace0849c7f5ec98eeb Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:28:03 +0100 Subject: [PATCH 081/184] try to get share-link nc27 -> os1 working --- .../nextcloud-v27-to-ocmstub-v1.cy.js | 81 +-- .../share-link/nextcloud-ocmstub.sh | 612 +++++++----------- 2 files changed, 262 insertions(+), 431 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index a372cc03..de84ae9b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -1,40 +1,17 @@ import { createShareLinkV27, - renameFileV27 + renameFileV27, + navigationSwitchLeftSideV27, + selectAppFromLeftSideV27, } from '../utils/nextcloud-v27' -import { - generateShareAssertions, -} from '../utils/ocmstub-v1.js'; - describe('Share link federated sharing functionality for Nextcloud', () => { - // Shared variables to avoid repetition and improve maintainability - const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; - const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; - const recipientDomain = 'ocmstub1.docker'; // recipientUrl.replace(/^https?:\/\/|\/$/g, ''); - const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; - const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; - const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; - const originalFileName = 'welcome.txt'; - const sharedFileName = 'nc1-to-os1-share-link.txt'; - - // Expected details of the federated share - const expectedShareDetails = { - shareWith: `${recipientUsername}@${recipientUrl}`, - fileName: sharedFileName, - owner: `${senderUsername}@${senderUrl}/`, - sender: `${senderUsername}@${senderUrl}/`, - shareType: 'user', - resourceType: 'file', - protocol: 'webdav' - }; - - it('Send federated share from Nextcloud v27 to ocmstub v1', () => { + it('Send federated share from Nextcloud v27 to Nextcloud v27', () => { // send share from Nextcloud 1. - cy.loginNextcloud(senderUrl, senderUsername, senderPassword) + cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') - renameFileV27(originalFileName, sharedFileName) - createShareLinkV27(sharedFileName).then( + renameFileV27('welcome.txt', 'nc1-to-nc2-share-link.txt') + createShareLinkV27('nc1-to-nc2-share-link.txt').then( (result) => { cy.visit(result) @@ -42,33 +19,31 @@ describe('Share link federated sharing functionality for Nextcloud', () => { cy.get('button[id="save-external-share"]').click() cy.get('form[class="save-form"]').within(() => { - cy.get('input[id="remote_address"]').type(`${recipientUsername}@${recipientDomain}`) + cy.get('input[id="remote_address"]').type('michiel@nextcloud2.docker') cy.get('input[id="save-button-confirm"]').click() }) } ) }) - it('Receive federated share from Nextcloud v27 to ocmstub v1', () => { - // Step 1: Log in to OcmStub - cy.loginOcmStub(recipientUrl); - - // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. - // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. - // Adjust these strings if the page format changes. - const shareAssertions = generateShareAssertions(expectedShareDetails); - - // FIXME: Remove this once https://github.com/nextcloud/server/issues/36340#issuecomment-2575333222 - // is resolved - shareAssertions['sharedBy'] = shareAssertions['sender']; - delete shareAssertions['sender']; - - // Step 2: Loop through all assertions and verify their presence on the page - // We use `cy.contains()` to search for the text anywhere on the page. - // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. - shareAssertions.forEach((assertion) => { - cy.contains(assertion, { timeout: 10000 }) - .should('be.visible'); - }); - }) + /** + * Test Case: Receiving a federated share on OcmStub from Nextcloud. + */ + it('Receive federated share of a file from from Nextcloud v27 to OcmStub v1', () => { + // Step 1: Log in to OcmStub + cy.loginOcmStub(recipientUrl); + + // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. + // Adjust these strings if the page format changes. + const shareAssertions = generateShareAssertions(expectedShareDetails); + + // Step 2: Loop through all assertions and verify their presence on the page + // We use `cy.contains()` to search for the text anywhere on the page. + // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. + shareAssertions.forEach((assertion) => { + cy.contains(assertion, { timeout: 10000 }) + .should('be.visible'); + }); + }); }) diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 82c3aa5e..2ad1d8b7 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -1,72 +1,131 @@ #!/usr/bin/env bash -# ----------------------------------------------------------------------------------- -# Script to Test Nextcloud to OcmStub OCM share-link flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Description: -# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms -# such as Nextcloud, OcmStub, using Cypress, and Docker containers. -# It supports both development and CI environments, with optional browser support. - -# Usage: -# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# @michielbdejong halt on error in docker init scripts. +set -e + +# find this scripts location. +SOURCE=${BASH_SOURCE[0]} +while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. + DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) + SOURCE=$(readlink "${SOURCE}") + # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. + [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" +done +DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) + +cd "${DIR}/../../.." || exit + +ENV_ROOT=$(pwd) +export ENV_ROOT=${ENV_ROOT} + +# nextcloud version: +# - v27.1.11 +# - v28.0.14 +EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} + +# nextcloud version: +# - v27.1.11 +# - v28.0.14 +EFSS_PLATFORM_2_VERSION=${2:-"v1.0.0"} + +# script mode: dev, ci. default is dev. +SCRIPT_MODE=${3:-"dev"} + +# browser platform: chrome, edge, firefox, electron. default is electron. +# only applies on SCRIPT_MODE=ci +BROWSER_PLATFORM=${4:-"electron"} + +function redirect_to_null_cmd() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} -# Arguments: -# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). -# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). -# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. -# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +function waitForPort () { + redirect_to_null_cmd echo waitForPort "${1} ${2}" + # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. + x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) + until [ "${x}" -ne 0 ] + do + redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" + sleep 1 + x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) + done + redirect_to_null_cmd echo "${1} port ${2} is open" +} -# Requirements: -# - Docker and required images must be installed. -# - Test scripts and configurations must be located in the expected directories. -# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. +function createEfss() { + local platform="${1}" + local number="${2}" + local user="${3}" + local password="${4}" + local init_script="${5}" + local tag="${6-latest}" + local image="${7}" + + if [[ -z "${image}" ]]; then + local image="pondersource/dev-stock-${platform}" + else + local image="pondersource/dev-stock-${platform}-${image}" + fi -# Example: -# ./nextcloud-ocmstub.sh v28.0.14 v1.0.0 ci electron + redirect_to_null_cmd echo "creating efss ${platform} ${number}" + + redirect_to_null_cmd docker run --detach --network=testnet \ + --name="maria${platform}${number}.docker" \ + -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ + mariadb:11.4.2 \ + --transaction-isolation=READ-COMMITTED \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed + + redirect_to_null_cmd docker run --detach --network=testnet \ + --name="${platform}${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="${platform}${number}" \ + -e DBHOST="maria${platform}${number}.docker" \ + -e USER="${user}" \ + -e PASS="${password}" \ + -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ + -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ + -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ + -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ + "${image}:${tag}" + + # wait for hostname port to be open. + waitForPort "maria${platform}${number}.docker" 3306 + waitForPort "${platform}${number}.docker" 443 + + # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) + docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 + docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 + docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 + docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 + docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 + + # run init script inside efss. + redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" + + redirect_to_null_cmd echo "" +} -# ----------------------------------------------------------------------------------- +# delete and create temp directory. +rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" -# Exit immediately if a command exits with a non-zero status, -# a variable is used but not defined, or a command in a pipeline fails -set -euo pipefail +# copy init files. +cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" -# ----------------------------------------------------------------------------------- -# Constants and Default Values -# ----------------------------------------------------------------------------------- +# auto clean before starting. +"${ENV_ROOT}/scripts/clean.sh" "no" -# Default versions -DEFAULT_EFSS_1_VERSION="v27.1.11" -DEFAULT_EFSS_2_VERSION="v1.0.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest +# make sure network exists. +docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 # ----------------------------------------------------------------------------------- -# Utility Functions +# Utility Functions Copied From dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- @@ -91,63 +150,6 @@ error_exit() { exit 1 } -# ----------------------------------------------------------------------------------- -# Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. -# ----------------------------------------------------------------------------------- -resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" -} - -# ----------------------------------------------------------------------------------- -# Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - # ----------------------------------------------------------------------------------- # Function: run_quietly_if_ci # Purpose: Run a command, suppressing stdout in CI mode. @@ -161,99 +163,6 @@ run_quietly_if_ci() { "$@" fi } - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - # ----------------------------------------------------------------------------------- # Function: create_ocmstub # Purpose: Create a OcmStub container. @@ -279,174 +188,121 @@ function create_ocmstub() { } -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi -} - -# ----------------------------------------------------------------------------------- -# Main Execution -# ----------------------------------------------------------------------------------- -main() { - # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi - - # Create EFSS containers - # # id # username # password # image # tag - create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" - create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" - - if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://ocmstub1.docker (just click 'Log in')" - - else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi - fi -} +################# +### Nextcloud ### +################# + +# syntax: +# createEfss platform number username password image. +# +# +# platform: owncloud, nextcloud. +# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. +# username: username for sign in into efss. +# password: password for sign in into efss. +# init script: script for initializing efss. +# tag: tag for the image, use latest if not sure. +# image: which image variation to use for container. + +# Nextclouds. +createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" +create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + +# disable cypress editing javascript files. it would make adding share to your own efss fail. +sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + +if [ "${SCRIPT_MODE}" = "dev" ]; then + ############### + ### Firefox ### + ############### + + docker run --detach --network=testnet \ + --name=firefox \ + -p 5800:5800 \ + --shm-size 2g \ + -e USER_ID="${UID}" \ + -e GROUP_ID="${UID}" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + jlesage/firefox:latest \ + >/dev/null 2>&1 + + ################## + ### VNC Server ### + ################## + + # remove previous x11 unix socket file, avoid any problems while mounting new one. + sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" + + # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, + # NOTE: please do not commit any change related to resolution. + docker run --detach --network=testnet \ + --name=vnc-server \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ + theasp/novnc:latest + + ############### + ### Cypress ### + ############### + + # create cypress and attach its display to the VNC server container. + # this way you can view inside cypress container through vnc server. + docker run --detach --network=testnet \ + --name="cypress.docker" \ + -e DISPLAY=vnc-server:0.0 \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + cypress/included:13.13.1 \ + open --project . + + # print instructions. + clear + echo "Now browse to :" + echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "Embedded Firefox -> http://localhost:5800" + echo "" + echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" + echo "https://nextcloud1.docker -> username: einstein password: relativity" + echo "https://nextcloud2.docker -> username: michiel password: dejong" +else + # only record when testing on electron. + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi + ################## + ### Cypress CI ### + ################## + + # extract version up until first dot . , example: v27.1.17 becomes v27 + P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" + P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" + + # for some reason this test fails on ci! so lets sleep a bit. + sleep 60 + + # run Cypress test suite headlessly and with the defined browser. + docker run --network=testnet \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + cypress/included:13.13.1 cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" + + # revert config file back to normal. + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi -# ----------------------------------------------------------------------------------- -# Execute the main function with passed arguments -# ----------------------------------------------------------------------------------- -main "$@" + # auto clean after running tests in ci mode. do not clear terminal. + "${ENV_ROOT}/scripts/clean.sh" "no" +fi From 2d1b0462f3eebcd2c6f900dfbef8c11c799a84b7 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:29:36 +0100 Subject: [PATCH 082/184] specify missing DOCKER_NETWORK env var --- dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 2ad1d8b7..4e2700f1 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -128,6 +128,9 @@ docker network inspect testnet >/dev/null 2>&1 || docker network create testnet # Utility Functions Copied From dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh # ----------------------------------------------------------------------------------- +# Docker network name +DOCKER_NETWORK="testnet" + # ----------------------------------------------------------------------------------- # Function: print_error # Purpose: Print an error message to stderr. From f4846a7aed53560f66f8595723887024109b88d7 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:36:16 +0100 Subject: [PATCH 083/184] Update script for dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh --- .../share-link/nextcloud-nextcloud.sh | 648 +++++++++++------- 1 file changed, 417 insertions(+), 231 deletions(-) diff --git a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh index ddc2c56f..dfdad6df 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh @@ -1,41 +1,160 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_2_VERSION=${2:-"v27.1.11"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to Nextcloud OCM share-link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-nextcloud.sh v28.0.14 v27.1.11 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v27.1.11" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Docker network name +DOCKER_NETWORK="testnet" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { if [ "${SCRIPT_MODE}" = "ci" ]; then "$@" >/dev/null 2>&1 else @@ -43,202 +162,269 @@ function redirect_to_null_cmd() { fi } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi } -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + + # disable cypress editing javascript files. it would make adding share to your own efss fail. + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://nextcloud2.docker (username: michiel, password: dejong)" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# Nextclouds. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" -createEfss nextcloud 2 michiel dejong nextcloud.sh "${EFSS_PLATFORM_2_VERSION}" - -# disable cypress editing javascript files. it would make adding share to your own efss fail. -sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://nextcloud2.docker -> username: michiel password: dejong" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # for some reason this test fails on ci! so lets sleep a bit. - sleep 60 - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From 4448112e6766829174aee13c3255ef10e9305186 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:44:24 +0100 Subject: [PATCH 084/184] share-link NC27 -> OS1, the new way --- cypress/ocm-test-suite/cypress.config.js | 2 +- .../nextcloud-v27-to-ocmstub-v1.cy.js | 4 + .../share-link/nextcloud-ocmstub.sh | 612 +++++++++++------- 3 files changed, 383 insertions(+), 235 deletions(-) diff --git a/cypress/ocm-test-suite/cypress.config.js b/cypress/ocm-test-suite/cypress.config.js index ebcf57c7..b65a4119 100644 --- a/cypress/ocm-test-suite/cypress.config.js +++ b/cypress/ocm-test-suite/cypress.config.js @@ -7,5 +7,5 @@ module.exports = defineConfig({ chromeWebSecurity: false, video: true, videoCompression: true, - modifyObstructiveCode: true, + modifyObstructiveCode: false, }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index de84ae9b..def55e97 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -38,6 +38,10 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // Adjust these strings if the page format changes. const shareAssertions = generateShareAssertions(expectedShareDetails); + // work around https://github.com/nextcloud/server/issues/36340 + shareAssertions['sharedBy'] = shareAssertions['sender']; + delete shareAssertions['sender']; + // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 4e2700f1..ad3f2064 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -1,136 +1,74 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_2_VERSION=${2:-"v1.0.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, OcmStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi +# Usage: +# ./nextcloud-ocmstub.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v1.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" +# Example: +# ./nextcloud-ocmstub.sh v28.0.14 v1.0.0 ci electron -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" +# ----------------------------------------------------------------------------------- -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail # ----------------------------------------------------------------------------------- -# Utility Functions Copied From dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh +# Constants and Default Values # ----------------------------------------------------------------------------------- +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v1.0.0" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + # Docker network name DOCKER_NETWORK="testnet" +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + # ----------------------------------------------------------------------------------- # Function: print_error # Purpose: Print an error message to stderr. @@ -153,6 +91,63 @@ error_exit() { exit 1 } +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# Returns: +# The absolute path to the script's directory. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "${source}" ]; do + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + source="$(readlink "${source}")" + # Resolve relative symlink + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + printf "%s" "${dir}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose: Initialize the environment and set global variables. +# ----------------------------------------------------------------------------------- +initialize_environment() { + local script_dir + script_dir="$(resolve_script_dir)" + cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." + ENV_ROOT="$(pwd)" + export ENV_ROOT="${ENV_ROOT}" + + # Ensure required commands are available + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + # ----------------------------------------------------------------------------------- # Function: run_quietly_if_ci # Purpose: Run a command, suppressing stdout in CI mode. @@ -166,6 +161,99 @@ run_quietly_if_ci() { "$@" fi } + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + # ----------------------------------------------------------------------------------- # Function: create_ocmstub # Purpose: Create a OcmStub container. @@ -191,121 +279,177 @@ function create_ocmstub() { } -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# Nextclouds. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" -create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" - -# disable cypress editing javascript files. it would make adding share to your own efss fail. -sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://nextcloud2.docker -> username: michiel password: dejong" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # for some reason this test fails on ci! so lets sleep a bit. - sleep 60 - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" +} - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment + parse_arguments "$@" + validate_files + + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources and ensure Docker network exists + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" + + # disable cypress editing javascript files. it would make adding share to your own efss fail. + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + echo "Setting up development environment..." + + # Start Firefox container + run_quietly_if_ci echo "Starting Firefox container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}":"${FIREFOX_TAG}" + + # Start VNC Server container + run_quietly_if_ci echo "Starting VNC Server..." + local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + # Ensure previous socket files are removed + remove_directory "${x11_socket}" + mkdir -p "${x11_socket}" + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${x11_socket}:/tmp/.X11-unix" \ + "${VNC_REPO}":"${VNC_TAG}" + + # Start Cypress container + echo "Starting Cypress container..." + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${x11_socket}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + open --project . + + # Display setup instructions + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" + echo " https://nextcloud1.docker (username: einstein, password: relativity)" + echo " https://ocmstub1.docker (just click 'Log in')" + + else + echo "Running tests in CI mode..." + + # Cypress configuration file + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Adjust Cypress configurations for non-default browser platforms + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" + local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Run Cypress tests in headless mode + echo "Running Cypress tests..." + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + + # Revert Cypress configuration changes + if [ "${BROWSER_PLATFORM}" != "electron" ]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From 7883e7870ce1396ed91f6e218dceaaca43b7daf2 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:46:41 +0100 Subject: [PATCH 085/184] Update share-with -> share-link in copied file --- dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index ad3f2064..b3db9e7d 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ----------------------------------------------------------------------------------- -# Script to Test Nextcloud to OcmStub OCM share-with flow tests. +# Script to Test Nextcloud to OcmStub OCM share-link flow tests. # Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- @@ -431,7 +431,7 @@ main() { "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ cypress run \ --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." + --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." # Revert Cypress configuration changes if [ "${BROWSER_PLATFORM}" != "electron" ]; then From 349cd62941cdb2a9bfe7cb4e36664934e882f05f Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 16:58:17 +0100 Subject: [PATCH 086/184] Use variables --- .../nextcloud-v27-to-ocmstub-v1.cy.js | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index def55e97..5bd9af2e 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -1,17 +1,40 @@ import { createShareLinkV27, renameFileV27, - navigationSwitchLeftSideV27, - selectAppFromLeftSideV27, -} from '../utils/nextcloud-v27' +} from '../utils/nextcloud-v27'; + +import { + generateShareAssertions, +} from '../utils/ocmstub-v1.js'; describe('Share link federated sharing functionality for Nextcloud', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCMSTUB1_USERNAME') || 'michiel'; + const originalFileName = 'welcome.txt'; + const sharedFileName = 'nc1-to-os1-share.txt'; + const sharedWith = 'michiel@ocmstub1.docker'; + + // Expected details of the federated share + const expectedShareDetails = { + shareWith: `${recipientUsername}@${recipientUrl}`, + fileName: sharedFileName, + owner: `${senderUsername}@${senderUrl}/`, + sender: `${senderUsername}@${senderUrl}/`, + shareType: 'user', + resourceType: 'file', + protocol: 'webdav' + }; + it('Send federated share from Nextcloud v27 to Nextcloud v27', () => { // send share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'einstein', 'relativity') + cy.loginNextcloud(senderUrl, senderUsername, senderPassword) - renameFileV27('welcome.txt', 'nc1-to-nc2-share-link.txt') - createShareLinkV27('nc1-to-nc2-share-link.txt').then( + renameFileV27(originalFileName, sharedFileName) + createShareLinkV27(sharedFileName).then( (result) => { cy.visit(result) @@ -19,7 +42,7 @@ describe('Share link federated sharing functionality for Nextcloud', () => { cy.get('button[id="save-external-share"]').click() cy.get('form[class="save-form"]').within(() => { - cy.get('input[id="remote_address"]').type('michiel@nextcloud2.docker') + cy.get('input[id="remote_address"]').type(sharedWith) cy.get('input[id="save-button-confirm"]').click() }) } From 5f183a06987a7ff4a4abcf7487fcd5b38a923b71 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Tue, 7 Jan 2025 17:01:50 +0100 Subject: [PATCH 087/184] allowNcDivergence --- cypress/ocm-test-suite/cypress.config.js | 2 +- .../e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 6 +----- cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cypress/ocm-test-suite/cypress.config.js b/cypress/ocm-test-suite/cypress.config.js index b65a4119..ebcf57c7 100644 --- a/cypress/ocm-test-suite/cypress.config.js +++ b/cypress/ocm-test-suite/cypress.config.js @@ -7,5 +7,5 @@ module.exports = defineConfig({ chromeWebSecurity: false, video: true, videoCompression: true, - modifyObstructiveCode: false, + modifyObstructiveCode: true, }) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index 5bd9af2e..5672617c 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -59,11 +59,7 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. - const shareAssertions = generateShareAssertions(expectedShareDetails); - - // work around https://github.com/nextcloud/server/issues/36340 - shareAssertions['sharedBy'] = shareAssertions['sender']; - delete shareAssertions['sender']; + const shareAssertions = generateShareAssertions(expectedShareDetails, true); // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js index 87bf94a3..316d2d07 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js @@ -39,7 +39,7 @@ * // The returned array can then be used with `cy.contains()` calls in Cypress tests: * // shareAssertions.forEach(assertion => cy.contains(assertion).should('be.visible')); */ -export function generateShareAssertions(expectedDetails) { +export function generateShareAssertions(expectedDetails, allowNcDivergence = false) { // Required fields that must be present and non-empty strings const requiredFields = [ 'shareWith', @@ -73,7 +73,7 @@ export function generateShareAssertions(expectedDetails) { `"providerId":`, `"shareType": "${expectedDetails.shareType}"`, `"owner": "${expectedDetails.owner}"`, - `"sender": "${expectedDetails.sender}"`, + (allowNcDivergence? `"sharedBy": "${expectedDetails.sender}"` : `"sender": "${expectedDetails.sender}"`), `"resourceType": "${expectedDetails.resourceType}"`, // For protocol, we know 'name' but 'sharedSecret' may vary. // We assert on part of the structure to ensure the protocol block is present. From b088930c8cb0badf963f77a13b2e27c05bb41e47 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 08:56:28 +0000 Subject: [PATCH 088/184] fix: JSONArgsRecommended by specifying a SHELL before CMD --- docker/dockerfiles/nextcloud.Dockerfile | 1 + docker/dockerfiles/owncloud.Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/dockerfiles/nextcloud.Dockerfile b/docker/dockerfiles/nextcloud.Dockerfile index 70c66711..e48d31f5 100644 --- a/docker/dockerfiles/nextcloud.Dockerfile +++ b/docker/dockerfiles/nextcloud.Dockerfile @@ -35,5 +35,6 @@ COPY ./scripts/nextcloud/*.sh / COPY ./scripts/nextcloud/upgrade.exclude / COPY ./configs/nextcloud/* /usr/src/nextcloud/config/ +SHELL ["/bin/bash", "-c"] ENTRYPOINT ["/entrypoint.sh"] CMD apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow /var/www/html/data/nextcloud.log diff --git a/docker/dockerfiles/owncloud.Dockerfile b/docker/dockerfiles/owncloud.Dockerfile index cfc14ffc..4adb6db2 100644 --- a/docker/dockerfiles/owncloud.Dockerfile +++ b/docker/dockerfiles/owncloud.Dockerfile @@ -39,5 +39,6 @@ COPY ./scripts/owncloud/*.sh / COPY ./scripts/owncloud/upgrade.exclude / COPY ./configs/owncloud/* /usr/src/owncloud/config/ +SHELL ["/bin/bash", "-c"] ENTRYPOINT ["/entrypoint.sh"] CMD apache2ctl -DFOREGROUND & tail --follow /var/log/apache2/access.log & tail --follow /var/log/apache2/error.log & tail --follow /var/www/html/data/owncloud.log From f5b962dbd89f6ad5beec37da9bbde4deea5ecde1 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 09:16:32 +0000 Subject: [PATCH 089/184] add: authors in header --- .../share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 16 ++++++++++++---- .../share-link/nextcloud-nextcloud.sh | 4 +++- .../share-link/nextcloud-ocmstub.sh | 4 +++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index 5672617c..533a609e 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -1,3 +1,11 @@ +/** + * @fileoverview + * Cypress test suite for testing federated sharing functionality via share-link flow in Nextcloud v27 and OcmStub v1. + * + * @author Michiel B. de Jong + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { createShareLinkV27, renameFileV27, @@ -7,7 +15,7 @@ import { generateShareAssertions, } from '../utils/ocmstub-v1.js'; -describe('Share link federated sharing functionality for Nextcloud', () => { +describe('Share link federated sharing functionality for Nextcloud to OcmStub', () => { // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; const recipientUrl = Cypress.env('OCMSTUB1_URL') || 'https://ocmstub1.docker'; @@ -29,7 +37,7 @@ describe('Share link federated sharing functionality for Nextcloud', () => { protocol: 'webdav' }; - it('Send federated share from Nextcloud v27 to Nextcloud v27', () => { + it('Send federated share from Nextcloud v27 to OcmStub v1', () => { // send share from Nextcloud 1. cy.loginNextcloud(senderUrl, senderUsername, senderPassword) @@ -56,12 +64,12 @@ describe('Share link federated sharing functionality for Nextcloud', () => { // Step 1: Log in to OcmStub cy.loginOcmStub(recipientUrl); - // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. + // Step 2: Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. const shareAssertions = generateShareAssertions(expectedShareDetails, true); - // Step 2: Loop through all assertions and verify their presence on the page + // Step 3: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. // The `should('be.visible')` ensures that the text is actually visible, not hidden or off-screen. shareAssertions.forEach((assertion) => { diff --git a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh index dfdad6df..010e5875 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh @@ -2,7 +2,9 @@ # ----------------------------------------------------------------------------------- # Script to Test Nextcloud to Nextcloud OCM share-link flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index b3db9e7d..3874ba4b 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -2,7 +2,9 @@ # ----------------------------------------------------------------------------------- # Script to Test Nextcloud to OcmStub OCM share-link flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- From 1c66697f3efea1d0ea24a15ecf829760cff8798e Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 09:17:02 +0000 Subject: [PATCH 090/184] add: argument in docstrings and TODO comment for future --- cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js index 316d2d07..ae938e40 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/ocmstub-v1.js @@ -18,6 +18,7 @@ * @param {string} expectedDetails.sender - The sender of the shared resource (e.g., "einstein@nextcloud.com"). * @param {string} expectedDetails.resourceType - The type of the shared resource ("file", "folder", etc.). * @param {string} expectedDetails.protocol - The protocol used for the share (e.g., "webdav"). + * @param {boolean} isNextcloud - Whether the instance being checked is nectcloud or not. * @returns {string[]} - An array of strings representing expected lines of text that should appear in the UI. * * @throws {Error} Throws an error if any required field in expectedDetails is missing or not a string. @@ -35,11 +36,11 @@ * }; * * // Generate the share assertions - * const shareAssertions = generateShareAssertions(expectedDetails); + * const shareAssertions = generateShareAssertions(expectedDetails, true); * // The returned array can then be used with `cy.contains()` calls in Cypress tests: * // shareAssertions.forEach(assertion => cy.contains(assertion).should('be.visible')); */ -export function generateShareAssertions(expectedDetails, allowNcDivergence = false) { +export function generateShareAssertions(expectedDetails, isNextcloud = false) { // Required fields that must be present and non-empty strings const requiredFields = [ 'shareWith', @@ -73,7 +74,10 @@ export function generateShareAssertions(expectedDetails, allowNcDivergence = fal `"providerId":`, `"shareType": "${expectedDetails.shareType}"`, `"owner": "${expectedDetails.owner}"`, - (allowNcDivergence? `"sharedBy": "${expectedDetails.sender}"` : `"sender": "${expectedDetails.sender}"`), + // Nextcloud is not following OCM specification at this moment, see: https://github.com/nextcloud/server/issues/36340#issuecomment-2575333222 + // this should be fixed via https://github.com/nextcloud/server/pull/50069 + // TODO @MahdiBaghbani: rename this to isLegacyNextcloud once the PR is merged and available in a new Nextcloud release. + (isNextcloud? `"sharedBy": "${expectedDetails.sender}"` : `"sender": "${expectedDetails.sender}"`), `"resourceType": "${expectedDetails.resourceType}"`, // For protocol, we know 'name' but 'sharedSecret' may vary. // We assert on part of the structure to ensure the protocol block is present. From 4afd52824d88c2453bc991250f1f920bc6363468 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 09:19:21 +0000 Subject: [PATCH 091/184] add: GtiHub actions test runner --- .github/workflows/share-link-nc-v27-os-v1.yml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/share-link-nc-v27-os-v1.yml diff --git a/.github/workflows/share-link-nc-v27-os-v1.yml b/.github/workflows/share-link-nc-v27-os-v1.yml new file mode 100644 index 00000000..2ad331fa --- /dev/null +++ b/.github/workflows/share-link-nc-v27-os-v1.yml @@ -0,0 +1,66 @@ +name: OCM Test Share Link NC v27.1.11 to OcmStub v1.0.0 + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the specified branch and files. + push: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + pull_request: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + # Allows you to run this workflow manually from the Actions tab. + workflow_dispatch: + +jobs: + share-link: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + sender: [ + { + platform: nextcloud, + version: v27.1.11 + }, + ] + receiver: [ + { + platform: ocmstub, + version: "v1.0.0" + }, + ] + + # The OS to run tests on, (I believe for OCM testing OS is really not that important). + runs-on: ubuntu-24.04 + + # Steps represent a sequence of tasks that will be executed as part of the job. + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. + - name: Checkout. + uses: actions/checkout@v4 + + - name: Pull images. + shell: bash + run: | + ./docker/pull/ocm-test-suite/${{ matrix.sender.platform }}.sh ${{ matrix.sender.version }} + ./docker/pull/ocm-test-suite/${{ matrix.receiver.platform }}.sh ${{ matrix.receiver.version }} + + - name: Run tests. + shell: bash + run: ./dev/ocm-test-suite.sh share-link ${{ matrix.sender.platform }} ${{ matrix.sender.version }} ci electron ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + + - name: Upload Cypress video artifacts. + uses: actions/upload-artifact@v4 + if: always() + with: + name: share-link from ${{ matrix.sender.platform }} ${{ matrix.sender.version }} to ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + path: ./cypress/ocm-test-suite/cypress/videos From 15d0424677e04d1b5dca81687f9b9383b9007bad Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 11:38:57 +0000 Subject: [PATCH 092/184] modify: use first instead of each --- .../ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index 8b05f427..b19cd537 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -43,14 +43,13 @@ export function acceptShareV27() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .each($div => { - cy.wrap($div).within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') + .first() + .within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') .find('button.primary') - // .should('be.visible') + .should('exist') .click({ force: true }); - }) }); } From 3c41f82b7d648899a4d0a18968804ab90960b409 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 11:44:40 +0000 Subject: [PATCH 093/184] add: authors --- .../cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js index f8638a25..ac2b66f7 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js @@ -3,6 +3,7 @@ * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and Nextcloud v27. * This suite verifies the ability to send and receive federated file shares between OcmStub and Nextcloud. * + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ From 8aa70a23ab89c34fa076f7f4993d8a0d4f452df8 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 12:00:20 +0000 Subject: [PATCH 094/184] add: GtiHub actions workflow --- .github/workflows/share-with-os-v1-nc-v27.yml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/share-with-os-v1-nc-v27.yml diff --git a/.github/workflows/share-with-os-v1-nc-v27.yml b/.github/workflows/share-with-os-v1-nc-v27.yml new file mode 100644 index 00000000..d4c8f1c0 --- /dev/null +++ b/.github/workflows/share-with-os-v1-nc-v27.yml @@ -0,0 +1,66 @@ +name: OCM Test Share With OcmStub v1.0.0 to NC v27.1.11 + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the specified branch and files. + push: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + pull_request: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + # Allows you to run this workflow manually from the Actions tab. + workflow_dispatch: + +jobs: + share-with: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + sender: [ + { + platform: ocmstub, + version: "v1.0.0" + }, + ] + receiver: [ + { + platform: nextcloud, + version: v27.1.11 + }, + ] + + # The OS to run tests on, (I believe for OCM testing OS is really not that important). + runs-on: ubuntu-24.04 + + # Steps represent a sequence of tasks that will be executed as part of the job. + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. + - name: Checkout. + uses: actions/checkout@v4 + + - name: Pull images. + shell: bash + run: | + ./docker/pull/ocm-test-suite/${{ matrix.sender.platform }}.sh ${{ matrix.sender.version }} + ./docker/pull/ocm-test-suite/${{ matrix.receiver.platform }}.sh ${{ matrix.receiver.version }} + + - name: Run tests. + shell: bash + run: ./dev/ocm-test-suite.sh share-with ${{ matrix.sender.platform }} ${{ matrix.sender.version }} ci electron ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + + - name: Upload Cypress video artifacts. + uses: actions/upload-artifact@v4 + if: always() + with: + name: share-with from ${{ matrix.sender.platform }} ${{ matrix.sender.version }} to ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + path: ./cypress/ocm-test-suite/cypress/videos From 41d8086a6f09c31a9a6b86b1bab2e8d56212c193 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 15:19:50 +0000 Subject: [PATCH 095/184] add: docs and ensure file exists --- .../nextcloud-v27-to-ocmstub-v1.cy.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index 533a609e..bce16036 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -37,11 +37,25 @@ describe('Share link federated sharing functionality for Nextcloud to OcmStub', protocol: 'webdav' }; + /** + * Test Case: Sending a federated share from one Nextcloud instance to OcmStub. + * Validates that a file can be successfully shared from Nextcloud to OcmStub. + */ it('Send federated share from Nextcloud v27 to OcmStub v1', () => { - // send share from Nextcloud 1. + // Step 1: Log in to the sender's Nextcloud instance cy.loginNextcloud(senderUrl, senderUsername, senderPassword) - renameFileV27(originalFileName, sharedFileName) + // Step 2: Ensure the original file exists before renaming + ensureFileExistsV27(originalFileName); + + // Step 3: Rename the file to prepare it for sharing + renameFileV27(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV27(sharedFileName); + + // Step 5: Create a federated share for the recipient + // TODO @MahdiBaghbani: We should hide any complexity in .cy.js files and move them to utils/*.js files createShareLinkV27(sharedFileName).then( (result) => { cy.visit(result) From 515da4d39d6229e05343e7586eab790a6db49e51 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 15:21:16 +0000 Subject: [PATCH 096/184] fix: authors header --- .../e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js | 4 ++-- .../e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js | 4 ++-- .../e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js | 4 ++-- .../e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js | 4 ++-- .../e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js | 5 +++-- .../cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js | 2 +- .../cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js | 6 ++++-- .../cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js | 2 +- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js index d58c63ca..eb031a53 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-ocmstub-v1.cy.js @@ -2,7 +2,7 @@ * @fileoverview * Cypress test suite for testing native federated sharing functionality in Nextcloud v27 and OcmStub v1. * - * @author Michiel De Jong + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ @@ -71,7 +71,7 @@ describe('Native Federated Sharing Functionality for Nextcloud to OcmStub', () = // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. - const shareAssertions = generateShareAssertions(expectedShareDetails); + const shareAssertions = generateShareAssertions(expectedShareDetails, true); // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js index 509cb1e7..b61f5a8d 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-ocmstub-v1.cy.js @@ -2,7 +2,7 @@ * @fileoverview * Cypress test suite for testing native federated sharing functionality in Nextcloud v28 and OcmStub v1. * - * @author Michiel De Jong + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ @@ -72,7 +72,7 @@ describe('Native Federated Sharing Functionality for Nextcloud to OcmStub', () = // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. - const shareAssertions = generateShareAssertions(expectedShareDetails); + const shareAssertions = generateShareAssertions(expectedShareDetails, true); // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js index a8b10f2d..997e956e 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v29-to-ocmstub-v1.cy.js @@ -2,7 +2,7 @@ * @fileoverview * Cypress test suite for testing native federated sharing functionality in Nextcloud v28 and OcmStub v1. * - * @author Michiel De Jong + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ @@ -68,7 +68,7 @@ describe('Native Federated Sharing Functionality for Nextcloud to OcmStub', () = // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. - const shareAssertions = generateShareAssertions(expectedShareDetails); + const shareAssertions = generateShareAssertions(expectedShareDetails, true); // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js index cf2ce46a..2e46fb91 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v30-to-ocmstub-v1.cy.js @@ -2,7 +2,7 @@ * @fileoverview * Cypress test suite for testing native federated sharing functionality in Nextcloud v28 and OcmStub v1. * - * @author Michiel De Jong + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ @@ -67,7 +67,7 @@ describe('Native Federated Sharing Functionality for Nextcloud to OcmStub', () = // Create an array of strings to verify. Each string is a snippet of text expected to be found on the page. // These assertions represent lines or properties that should appear in the OcmStub's displayed share metadata. // Adjust these strings if the page format changes. - const shareAssertions = generateShareAssertions(expectedShareDetails); + const shareAssertions = generateShareAssertions(expectedShareDetails, true); // Step 2: Loop through all assertions and verify their presence on the page // We use `cy.contains()` to search for the text anywhere on the page. diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js index ac2b66f7..83641493 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v27.cy.js @@ -23,9 +23,10 @@ describe('Federated sharing functionality from OcmStub to Nextcloud', () => { const sharedFileName = 'from-stub.txt'; /** - * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + * Test Case: Sending a federated share from OcmStub to Nextcloud. + * Validates that a file can be successfully shared from OcmStub to Nextcloud. */ - it('should successfully send a federated share of a file from OcmStub v1 to Nextcloud v27', () => { + it('Send a federated share of a file from OcmStub v1 to Nextcloud v27', () => { // Step 1: Navigate to the federated share link on OcmStub 1.0 // Remove trailing slash and leading https or http from recipientUrl cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js index bf61ea7c..67cc5596 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js @@ -2,7 +2,7 @@ * @fileoverview * Cypress test suite for testing native federated sharing functionality in OcmStub. * - * @author Michiel De Jong + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js index 9b0c7933..cfb2feb7 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js @@ -3,6 +3,7 @@ * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and ownCloud v10. * This suite verifies the ability to send and receive federated file shares between OcmStub and ownCloud. * + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ @@ -23,9 +24,10 @@ describe('Federated sharing functionality from OcmStub to ownCloud', () => { const sharedFileName = 'from-stub.txt'; /** - * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + * Test Case: Sending a federated share from OcmStub to ownCloud . + * Validates that a file can be successfully shared from OcmStub to ownCloud. */ - it('should successfully send a federated share of a file from OcmStub v1 to ownCloud v10', () => { + it('Send a federated share of a file from OcmStub v1 to ownCloud v10', () => { // Step 1: Navigate to the federated share link on OcmStub 1.0 // Remove trailing slash and leading https or http from recipientUrl cy.visit(`${senderUrl}/shareWith?${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`); diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js index 958e895b..e656098e 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js @@ -2,7 +2,7 @@ * @fileoverview * Cypress test suite for testing native federated sharing functionality in ownCloud v10 and OcmStub v1. * - * @author Michiel De Jong + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ From bf8674beb35c4c322f1869340c38afa53d31e3d7 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 15:28:11 +0000 Subject: [PATCH 097/184] fix: comments and header --- .../cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js index d191c0c6..284740f8 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-nextcloud-v28.cy.js @@ -3,6 +3,7 @@ * Cypress test suite for testing native federated sharing functionality in OcmStub v1 and Nextcloud v28. * This suite verifies the ability to send and receive federated file shares between OcmStub and Nextcloud. * + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ @@ -22,7 +23,8 @@ describe('Federated sharing functionality from OcmStub to Nextcloud', () => { const sharedFileName = 'from-stub.txt'; /** - * Test Case: Sending a federated share from OcmStub 1.0 to OcmStub 1.0. + * Test Case: Sending a federated share from OcmStub to Nextcloud. + * Validates that a file can be successfully shared from OcmStub to Nextcloud. */ it('should successfully send a federated share of a file from OcmStub v1 to Nextcloud v28', () => { // Step 1: Navigate to the federated share link on OcmStub 1.0 From 158059726c5c5078d5e7002f03ef3856f2234923 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 15:29:36 +0000 Subject: [PATCH 098/184] add: GitHub Actions workflows --- .github/workflows/share-with-os-v1-nc-v28.yml | 66 +++++++++++++++++++ .github/workflows/share-with-os-v1-oc-10.yml | 66 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 .github/workflows/share-with-os-v1-nc-v28.yml create mode 100644 .github/workflows/share-with-os-v1-oc-10.yml diff --git a/.github/workflows/share-with-os-v1-nc-v28.yml b/.github/workflows/share-with-os-v1-nc-v28.yml new file mode 100644 index 00000000..8d6fc699 --- /dev/null +++ b/.github/workflows/share-with-os-v1-nc-v28.yml @@ -0,0 +1,66 @@ +name: OCM Test Share With OcmStub v1.0.0 to NC v27.1.11 + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the specified branch and files. + push: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + pull_request: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + # Allows you to run this workflow manually from the Actions tab. + workflow_dispatch: + +jobs: + share-with: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + sender: [ + { + platform: ocmstub, + version: "v1.0.0" + }, + ] + receiver: [ + { + platform: nextcloud, + version: v28.0.14 + }, + ] + + # The OS to run tests on, (I believe for OCM testing OS is really not that important). + runs-on: ubuntu-24.04 + + # Steps represent a sequence of tasks that will be executed as part of the job. + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. + - name: Checkout. + uses: actions/checkout@v4 + + - name: Pull images. + shell: bash + run: | + ./docker/pull/ocm-test-suite/${{ matrix.sender.platform }}.sh ${{ matrix.sender.version }} + ./docker/pull/ocm-test-suite/${{ matrix.receiver.platform }}.sh ${{ matrix.receiver.version }} + + - name: Run tests. + shell: bash + run: ./dev/ocm-test-suite.sh share-with ${{ matrix.sender.platform }} ${{ matrix.sender.version }} ci electron ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + + - name: Upload Cypress video artifacts. + uses: actions/upload-artifact@v4 + if: always() + with: + name: share-with from ${{ matrix.sender.platform }} ${{ matrix.sender.version }} to ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + path: ./cypress/ocm-test-suite/cypress/videos diff --git a/.github/workflows/share-with-os-v1-oc-10.yml b/.github/workflows/share-with-os-v1-oc-10.yml new file mode 100644 index 00000000..507f6652 --- /dev/null +++ b/.github/workflows/share-with-os-v1-oc-10.yml @@ -0,0 +1,66 @@ +name: OCM Test Share With OcmStub v1.0.0 to NC v27.1.11 + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the specified branch and files. + push: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + pull_request: + branches: + - main + paths: + - "cypress/ocm-test-suite/**" + - "dev/ocm-test-suite.sh" + - "dev/ocm-test-suite/**" + # Allows you to run this workflow manually from the Actions tab. + workflow_dispatch: + +jobs: + share-with: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + sender: [ + { + platform: ocmstub, + version: "v1.0.0" + }, + ] + receiver: [ + { + platform: owncloud, + version: v10.15.0 + }, + ] + + # The OS to run tests on, (I believe for OCM testing OS is really not that important). + runs-on: ubuntu-24.04 + + # Steps represent a sequence of tasks that will be executed as part of the job. + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. + - name: Checkout. + uses: actions/checkout@v4 + + - name: Pull images. + shell: bash + run: | + ./docker/pull/ocm-test-suite/${{ matrix.sender.platform }}.sh ${{ matrix.sender.version }} + ./docker/pull/ocm-test-suite/${{ matrix.receiver.platform }}.sh ${{ matrix.receiver.version }} + + - name: Run tests. + shell: bash + run: ./dev/ocm-test-suite.sh share-with ${{ matrix.sender.platform }} ${{ matrix.sender.version }} ci electron ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + + - name: Upload Cypress video artifacts. + uses: actions/upload-artifact@v4 + if: always() + with: + name: share-with from ${{ matrix.sender.platform }} ${{ matrix.sender.version }} to ${{ matrix.receiver.platform }} ${{ matrix.receiver.version }} + path: ./cypress/ocm-test-suite/cypress/videos From 7972b516d8ba2dfd31c04abfd166570aaca78cd6 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Wed, 8 Jan 2025 17:04:33 +0100 Subject: [PATCH 099/184] Remove .each clauses --- .../cypress/e2e/utils/nextcloud-v27.js | 1 - .../cypress/e2e/utils/nextcloud-v28.js | 14 ++++++-------- .../ocm-test-suite/cypress/e2e/utils/owncloud.js | 14 ++++++-------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index b19cd537..628d1475 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -43,7 +43,6 @@ export function acceptShareV27() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .first() .within(() => { // Locate the button row and click the primary button cy.get('div.oc-dialog-buttonrow') diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js index 443dc11b..a016889f 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js @@ -43,14 +43,12 @@ export function acceptShareV28() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .each($div => { - cy.wrap($div).within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') - .find('button.primary') - // .should('be.visible') - .click({ force: true }); - }) + .within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') + .find('button.primary') + // .should('be.visible') + .click({ force: true }); }); } diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index 1b8a7ca7..fefbe455 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -43,14 +43,12 @@ export function acceptShare() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') - .each($div => { - cy.wrap($div).within(() => { - // Locate the button row and click the primary button - cy.get('div.oc-dialog-buttonrow') - .find('button.primary') - // .should('be.visible') - .click({ force: true }); - }) + .within(() => { + // Locate the button row and click the primary button + cy.get('div.oc-dialog-buttonrow') + .find('button.primary') + // .should('be.visible') + .click({ force: true }); }); } From c189dc06a75e1f3663d1faf20f08ab2ed4dc2b5b Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 16:18:05 +0000 Subject: [PATCH 100/184] add: .first on accept share --- cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js | 1 + cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js | 1 + cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js | 1 + 3 files changed, 3 insertions(+) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index 628d1475..b19cd537 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -43,6 +43,7 @@ export function acceptShareV27() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') + .first() .within(() => { // Locate the button row and click the primary button cy.get('div.oc-dialog-buttonrow') diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js index a016889f..4792b5f3 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js @@ -43,6 +43,7 @@ export function acceptShareV28() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') + .first() .within(() => { // Locate the button row and click the primary button cy.get('div.oc-dialog-buttonrow') diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index fefbe455..32a2a8c4 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -43,6 +43,7 @@ export function acceptShare() { // Wait for the share dialog to appear and ensure it's visible cy.get('div.oc-dialog', { timeout: 10000 }) .should('be.visible') + .first() .within(() => { // Locate the button row and click the primary button cy.get('div.oc-dialog-buttonrow') From c80379bbe8f30f67eab3a6adba3d91f9b7518496 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 16:20:53 +0000 Subject: [PATCH 101/184] style: format code --- .../ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js | 6 +++--- cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js index 4792b5f3..129ff191 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js @@ -47,9 +47,9 @@ export function acceptShareV28() { .within(() => { // Locate the button row and click the primary button cy.get('div.oc-dialog-buttonrow') - .find('button.primary') - // .should('be.visible') - .click({ force: true }); + .find('button.primary') + .should('exist') + .click({ force: true }); }); } diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index 32a2a8c4..02cc2495 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -47,9 +47,9 @@ export function acceptShare() { .within(() => { // Locate the button row and click the primary button cy.get('div.oc-dialog-buttonrow') - .find('button.primary') - // .should('be.visible') - .click({ force: true }); + .find('button.primary') + .should('exist') + .click({ force: true }); }); } @@ -366,8 +366,8 @@ export function selectAppFromLeftSide(appId) { cy.get('div#app-navigation', { timeout: 10000 }) .should('be.visible') .find(`li[data-id="${appId}"]`) - // .should('be.visible') - .click({force: true}); + .should('exist') + .click({ force: true }); } /** From 6fc025bb7344a2d1d3f228598107716c69724993 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 8 Jan 2025 16:22:13 +0000 Subject: [PATCH 102/184] add: authors to header --- cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js | 1 + cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js | 1 + cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js | 1 + 3 files changed, 3 insertions(+) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index b19cd537..19dbccd2 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -4,6 +4,7 @@ * These functions provide abstractions for common actions such as sharing files, * updating permissions, renaming files, and navigating the UI. * + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js index 129ff191..403e8c50 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v28.js @@ -4,6 +4,7 @@ * These functions provide abstractions for common actions such as sharing files, * updating permissions, renaming files, and navigating the UI. * + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index 02cc2495..109a71ed 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -4,6 +4,7 @@ * These functions provide abstractions for common actions such as accepting shares, * creating federated shares, renaming files, and interacting with the file menu. * + * @author Michiel B. de Jong * @author Mohammad Mahdi Baghbani Pourvahid */ From 7f662274b0f04f2f135dd69b1f5cb482959d966d Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 10 Jan 2025 10:42:50 +0000 Subject: [PATCH 103/184] add: forensic and mod security to owncloud, update headers --- docker/configs/owncloud/apache.conf | 2 ++ docker/dockerfiles/nextcloud-base.Dockerfile | 4 ++-- docker/dockerfiles/owncloud-base.Dockerfile | 12 +++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docker/configs/owncloud/apache.conf b/docker/configs/owncloud/apache.conf index a106f0ff..e311e925 100644 --- a/docker/configs/owncloud/apache.conf +++ b/docker/configs/owncloud/apache.conf @@ -21,6 +21,8 @@ Header always set Strict-Transport-Security "max-age=63072000;" + ForensicLog forensic.log + SSLEngine on SSLCertificateFile "/tls/server.crt" SSLCertificateKeyFile "/tls/server.key" diff --git a/docker/dockerfiles/nextcloud-base.Dockerfile b/docker/dockerfiles/nextcloud-base.Dockerfile index 69ae93bd..fe1f1e4c 100644 --- a/docker/dockerfiles/nextcloud-base.Dockerfile +++ b/docker/dockerfiles/nextcloud-base.Dockerfile @@ -5,7 +5,7 @@ FROM php:8.2.26-apache-bookworm@sha256:b8d8c9d7882fdea9d2ef5b3829bf9e34fb368f833 LABEL org.opencontainers.image.licenses=MIT LABEL org.opencontainers.image.title="PonderSource Nextcloud Base Image" LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" -LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" +LABEL org.opencontainers.image.authors="Michiel B. de Jong,Mohammad Mahdi Baghbani Pourvahid" # entrypoint.sh and cron.sh dependencies RUN set -ex; \ @@ -21,8 +21,8 @@ RUN set -ex; \ busybox-static \ libldap-common \ ca-certificates \ - libmagickcore-6.q16-6-extra \ libapache2-mod-security2 \ + libmagickcore-6.q16-6-extra \ ; \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ diff --git a/docker/dockerfiles/owncloud-base.Dockerfile b/docker/dockerfiles/owncloud-base.Dockerfile index 8b77296c..d9144c60 100644 --- a/docker/dockerfiles/owncloud-base.Dockerfile +++ b/docker/dockerfiles/owncloud-base.Dockerfile @@ -5,7 +5,7 @@ FROM php:7.4.33-apache-bullseye@sha256:c9d7e608f73832673479770d66aacc8100011ec75 LABEL org.opencontainers.image.licenses=MIT LABEL org.opencontainers.image.title="PonderSource ownCloud Base Image" LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" -LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" +LABEL org.opencontainers.image.authors="Michiel B. de Jong,Mohammad Mahdi Baghbani Pourvahid" # entrypoint.sh and cron.sh dependencies RUN set -ex; \ @@ -21,6 +21,7 @@ RUN set -ex; \ busybox-static \ libldap-common \ ca-certificates \ + libapache2-mod-security2 \ libmagickcore-6.q16-6-extra \ ; \ apt-get clean; \ @@ -99,6 +100,15 @@ RUN set -ex; \ apt-get clean; \ rm -rf /var/lib/apt/lists/* +RUN { \ + echo 'SecRuleEngine On'; \ + echo 'SecAuditEngine On'; \ + echo 'SecAuditLog /var/log/apache2/modsec_audit.log'; \ + echo 'SecRequestBodyAccess on'; \ + echo 'SecResponseBodyAccess On'; \ + echo 'SecAuditLogParts ABIJEFHZ'; \ + } > "/etc/modsecurity/modsecurity.conf"; + # set recommended PHP.ini settings RUN { \ echo 'opcache.enable=1'; \ From a6c15f825bdb7b9a59d5529f652a98ee62af7987 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 10 Jan 2025 10:43:03 +0000 Subject: [PATCH 104/184] update: headers --- dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh | 4 +++- dev/ocm-test-suite/share-with/ocmstub-owncloud.sh | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh index 495b026f..f57648d8 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh @@ -2,7 +2,9 @@ # ----------------------------------------------------------------------------------- # Script to Test OcmStub to Nextcloud OCM share-with flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- diff --git a/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh b/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh index 31000a78..2cb38083 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh @@ -2,7 +2,9 @@ # ----------------------------------------------------------------------------------- # Script to Test OcmStub to Nextcloud OCM share-with flow tests. -# Author: Mohammad Mahdi Baghbani Pourvahid +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------- From 752a63369593aa0a6db950bc46c7805a944ddc10 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 10 Jan 2025 15:21:16 +0000 Subject: [PATCH 105/184] fix: bug in the share name --- .../cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js index 67cc5596..7af6d473 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-ocmstub-v1.cy.js @@ -18,11 +18,12 @@ describe('Native federated sharing functionality for OcmStub', () => { const senderUsername = Cypress.env('OCMSTUB1_USERNAME') || 'einstein'; const recipientUsername = Cypress.env('OCMSTUB2_USERNAME') || 'mahdi'; const expectedMessage = 'yes shareWith'; + const sharedFileName = 'from-stub.txt'; // Expected details of the federated share const expectedShareDetails = { shareWith: `${recipientUsername}@${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, - fileName: 'Test share from stub', + fileName: `${sharedFileName}`, owner: `${senderUsername}@${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`, sender: `${senderUsername}@${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`, shareType: 'user', From fba42fd0f4932a8dbc93f8b3e1717121fb0095d1 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 10 Jan 2025 15:54:18 +0000 Subject: [PATCH 106/184] add: utils script --- scripts/utils.sh | 707 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 707 insertions(+) create mode 100755 scripts/utils.sh diff --git a/scripts/utils.sh b/scripts/utils.sh new file mode 100755 index 00000000..87a627db --- /dev/null +++ b/scripts/utils.sh @@ -0,0 +1,707 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to hold all the utility functions needed in the Dev Stock. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="${1}" +DEFAULT_EFSS_2_VERSION="${2}" +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" +export DEFAULT_EFSS_1_VERSION="${DEFAULT_EFSS_1_VERSION}" +export DEFAULT_EFSS_2_VERSION="${DEFAULT_EFSS_2_VERSION}" +export DEFAULT_SCRIPT_MODE="${DEFAULT_SCRIPT_MODE}" +export DEFAULT_BROWSER_PLATFORM="${DEFAULT_BROWSER_PLATFORM}" + +# Docker network name +DOCKER_NETWORK="testnet" +export DOCKER_NETWORK="${DOCKER_NETWORK}" + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" +export MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" +export TEMP_DIR="${TEMP_DIR}" +export TLS_CA_DIR="${TLS_CA_DIR}" +export TLS_CERTIFICATES_DIR="${TLS_CERTIFICATES_DIR}" + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest +export CYPRESS_REPO="${CYPRESS_REPO}" +export CYPRESS_TAG="${CYPRESS_TAG}" +export FIREFOX_REPO="${FIREFOX_REPO}" +export FIREFOX_TAG="${FIREFOX_TAG}" +export MARIADB_REPO="${MARIADB_REPO}" +export MARIADB_TAG="${MARIADB_TAG}" +export VNC_REPO="${VNC_REPO}" +export VNC_TAG="${VNC_TAG}" + +# ----------------------------------------------------------------------------------- +# Utility Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: print_error +# Purpose: Print an error message to stderr. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# ----------------------------------------------------------------------------------- +# Function: error_exit +# Purpose: Print an error message and exit with code 1. +# Arguments: +# $1 - The error message to display. +# ----------------------------------------------------------------------------------- +error_exit() { + print_error "${1}" + exit 1 +} + +# ----------------------------------------------------------------------------------- +# Function: wait_for_port +# Purpose: Wait for a Docker container to open a specific port. +# Arguments: +# $1 - The name of the Docker container. +# $2 - The port number to check. +# ----------------------------------------------------------------------------------- +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} + +# ----------------------------------------------------------------------------------- +# Function: run_quietly_if_ci +# Purpose: Run a command, suppressing stdout in CI mode. +# Arguments: +# $@ - The command and arguments to execute. +# ----------------------------------------------------------------------------------- +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: run_docker_container +# Purpose: Start a Docker container with the provided arguments. +# Arguments: +# $@ - Docker run command arguments +# ----------------------------------------------------------------------------------- +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# ----------------------------------------------------------------------------------- +# Function: remove_directory +# Purpose: Safely remove a directory if it exists. +# Arguments: +# $1 - Directory path +# ----------------------------------------------------------------------------------- +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: command_exists +# Purpose: Check if a command exists on the system. +# Arguments: +# $1 - The command to check. +# Returns: +# 0 if the command exists, 1 otherwise. +# ----------------------------------------------------------------------------------- +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------------- +# Function: ensure_required_commands +# Purpose : Ensure that certain commands (e.g., docker) are available on the system. +# Arguments: (none) +# Returns : +# Calls error_exit if any required command is missing. +# ----------------------------------------------------------------------------------- +ensure_required_commands() { + # Ensure required commands are available (here, just 'docker') + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# ----------------------------------------------------------------------------------- +# Function: ensure_docker_running +# Purpose : +# 1) Verify that the Docker daemon is running and accessible (e.g., user permissions). +# +# Arguments: +# (none) +# +# Returns : +# Exits with an error if Docker is either not installed or not running. +# ----------------------------------------------------------------------------------- +ensure_docker_running() { + # Check if the Docker daemon is running (or user has permission) + # 'docker info' returns non-zero if the daemon is not reachable. + if ! docker info >/dev/null 2>&1; then + error_exit "Cannot connect to the Docker daemon. Is it running and do you have the right permissions?" + fi +} + +# ----------------------------------------------------------------------------------- +# Setup Functions +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Function: extract_platform_variables +# Purpose : +# 1) Use the script's own file path to determine: +# - TEST_SCENARIO (from its parent directory) +# - EFSS_PLATFORM_1 and EFSS_PLATFORM_2 (from the file name minus .sh) +# 2) Export the resulting variables for use in the rest of the script. +# +# Arguments: +# (none) +# Returns : +# Exports TEST_SCENARIO, EFSS_PLATFORM_1, EFSS_PLATFORM_2 +# ----------------------------------------------------------------------------------- +extract_platform_variables() { + # Extract the parent folder name as TEST_SCENARIO + local test_scenario + test_scenario="$(basename "$(dirname "${SOURCE}")")" + + # Extract the filename without the .sh extension + local filename + filename="$(basename "${SOURCE}" .sh)" + + # Split the filename by '-' to get EFSS_PLATFORM_1 and EFSS_PLATFORM_2 + local platform1 platform2 + IFS='-' read -r platform1 platform2 <<< "${filename}" + + # Export the variables so the rest of the script can use them + export TEST_SCENARIO="${test_scenario}" + export EFSS_PLATFORM_1="${platform1}" + export EFSS_PLATFORM_2="${platform2}" +} + +# ----------------------------------------------------------------------------------- +# Function: parse_arguments +# Purpose: Parse command-line arguments and set global variables. +# Arguments: +# $@ - Command-line arguments +# ----------------------------------------------------------------------------------- +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" + + export EFSS_PLATFORM_1_VERSION="${EFSS_PLATFORM_1_VERSION}" + export EFSS_PLATFORM_2_VERSION="${EFSS_PLATFORM_2_VERSION}" + export SCRIPT_MODE="${SCRIPT_MODE}" + export BROWSER_PLATFORM="${BROWSER_PLATFORM}" +} + +# ----------------------------------------------------------------------------------- +# Function: validate_files +# Purpose: Validate that required files and directories exist. +# ----------------------------------------------------------------------------------- +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: prepare_environment +# Purpose: 1) Prepare temporary directories and copy necessary files +# 2) Run cleanup script (if it exists) +# 3) Ensure the specified Docker network is available +# ----------------------------------------------------------------------------------- +prepare_environment() { + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources (if the cleanup script is available) + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + # Ensure Docker network exists + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || + error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi +} + +# ----------------------------------------------------------------------------------- +# Function: setup_initial_environment +# Purpose : +# 1) Extract platform-dependent variables from the script path. +# 2) Ensure required commands (and Docker daemon) are running. +# 3) Parse command-line arguments. +# 4) Validate necessary files. +# 5) Prepare the environment (e.g., set up networks, clean old resources). +# +# Arguments: +# * - All arguments passed to the script (forwarded to parse_arguments). +# +# Returns : None. Exits on error if a required step fails. +# ----------------------------------------------------------------------------------- +setup() { + # Get platform dependent variables. + extract_platform_variables + + # Ensure required commands (including Docker) are available. + ensure_required_commands + ensure_docker_running + + # Parse CLI arguments + parse_arguments "$@" + + # Validate required files/directories + validate_files + + # Prepare the environment + prepare_environment +} + +# ----------------------------------------------------------------------------------- +# Function: print_ocm_test_setup_instructions +# Purpose : Print messages indicating that the development environment is ready, +# along with URLs and usage notes. +# Arguments: (none) +# Returns : (none) +# ----------------------------------------------------------------------------------- +print_ocm_test_setup_instructions() { + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" +} + +# ----------------------------------------------------------------------------------- +# Function: create_firefox +# Purpose : Launch a Firefox container with the necessary environment variables, +# volume mounts, and network configuration. +# Arguments: (none) +# Returns : (none) +# +# Example Usage: +# create_firefox +# ----------------------------------------------------------------------------------- +create_firefox() { + # Print message (quiet in CI mode) + run_quietly_if_ci echo "Starting Firefox container..." + + # Run Docker container with the specified parameters + run_docker_container --detach \ + --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}:${FIREFOX_TAG}" || error_exit "Failed to start Firefox." +} + +# ----------------------------------------------------------------------------------- +# Function: create_vnc +# Purpose : Launch a VNC server container with the necessary environment variables, +# volume mounts, and network configuration. +# Arguments: (none) +# Returns : (none) +# +# Example Usage: +# create_vnc +# ----------------------------------------------------------------------------------- +create_vnc() { + # Print message (quiet in CI mode) + run_quietly_if_ci echo "Starting VNC Server..." + + # Define path to the X11 socket directory + X11_SOCKET="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + export X11_SOCKET="${X11_SOCKET}" + + # Clean up any previous socket files and create a new directory + remove_directory "${X11_SOCKET}" + mkdir -p "${X11_SOCKET}" + + # Launch the VNC server container + run_docker_container --detach \ + --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${X11_SOCKET}:/tmp/.X11-unix" \ + "${VNC_REPO}:${VNC_TAG}" || error_exit "Failed to start VNC Server." +} + +# ----------------------------------------------------------------------------------- +# Function: create_cypress_dev +# Purpose : Launch a Cypress container with the necessary environment variables, +# volume mounts, and network configuration. +# Arguments: (none) +# Returns : (none) +# +# Requirements: +# - Environment vars: DOCKER_NETWORK, ENV_ROOT, X11_SOCKET, CYPRESS_REPO, CYPRESS_TAG +# - External function: run_docker_container +# +# Example Usage: +# create_cypress_dev +# ----------------------------------------------------------------------------------- +create_cypress_dev() { + # Print message (quiet in CI mode) + run_quietly_if_ci echo "Starting Cypress container..." + + run_docker_container --detach \ + --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${X11_SOCKET}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}:${CYPRESS_TAG}" \ + open --project . || error_exit "Failed to start Cypress." +} + +# ----------------------------------------------------------------------------------- +# Function: create_cypress_ci +# Purpose : Run Cypress tests in headless mode with the specified parameters. +# Arguments: +# 1) ${1} - The Cypress spec path (relative path to the spec file). +# Returns : (none) - exits on error +# +# Usage Example: +# create_cypress_ci "cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v28.cy.js" +# ----------------------------------------------------------------------------------- +create_cypress_ci() { + local cypress_spec="${1}" + + if [[ -z "$cypress_spec" ]]; then + error_exit "No Cypress spec provided. Usage: create_cypress_ci " + fi + + # Print message (quiet in CI mode) + run_quietly_if_ci echo "Running Cypress tests using spec: $cypress_spec" + + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}:${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "${cypress_spec}" || + error_exit "Cypress tests failed." +} + +# ----------------------------------------------------------------------------------- +# Function: create_nextcloud +# Purpose: Create a Nextcloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_owncloud +# Purpose: Create a ownCloud container with a MariaDB backend. +# Arguments: +# $1 - Instance number. +# $2 - Admin username. +# $3 - Admin password. +# $4 - Image name. +# $5 - Image tag. +# ----------------------------------------------------------------------------------- +create_owncloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaowncloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaowncloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="owncloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="owncloud${number}" \ + -e OWNCLOUD_HOST="owncloud${number}.docker" \ + -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ + -e OWNCLOUD_ADMIN_USER="${user}" \ + -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ + -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="mariaowncloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: create_ocmstub +# Purpose: Create a OcmStub container. +# Arguments: +# $1 - Instance number. +# $2 - Image name. +# $3 - Image tag. +# ----------------------------------------------------------------------------------- +function create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} + +# ----------------------------------------------------------------------------------- +# Function: run_dev +# Purpose : +# 1) Quietly log environment setup when in CI. +# 2) Create Firefox, VNC, and Cypress containers in dev mode. +# 3) Print OCM test setup instructions. +# 4) Echo two additional lines (supplied as arguments), e.g. EFSS login URLs. +# +# Arguments: +# 1) $1 - The first line to echo (e.g., "https://nextcloud1.docker (username...)") +# 2) $2 - The second line to echo (e.g., "https://nextcloud2.docker (username...)") +# +# Example Usage: +# run_dev \ +# "https://nextcloud1.docker (username: einstein, password: relativity)" \ +# "https://nextcloud2.docker (username: michiel, password: dejong)" +# +# Returns : None +# ----------------------------------------------------------------------------------- +run_dev() { + local url_line_1="${1}" + local url_line_2="${2}" + + # Quiet log in CI mode + run_quietly_if_ci echo "Setting up development environment..." + + # Create containers for Firefox, VNC, and Cypress (dev mode) + create_firefox + create_vnc + create_cypress_dev + + # Display setup instructions + print_ocm_test_setup_instructions + + # Echo the two lines passed as arguments + echo " ${url_line_1}" + echo " ${url_line_2}" +} + +# ----------------------------------------------------------------------------------- +# Function: run_ci +# Purpose : +# 1) Update Cypress config based on the chosen browser platform (disables video +# unless the BROWSER_PLATFORM is "electron"). +# 2) Compute major EFSS platform version numbers and run Cypress tests headlessly, +# using a test scenario path that is dynamically formed. +# 3) Revert Cypress config changes and perform a cleanup of the environment. +# +# Arguments: +# 1) $1 - The test scenario folder name (sub-path under cypress/e2e/). +# 2) $2 - The EFSS platform 1 name (e.g., "nextcloud"). +# 3) $3 - The EFSS platform 2 name (e.g., "nextcloud", "owncloud", etc.). +# +# Returns : +# None. Exits (via error_exit) on critical failure. +# ----------------------------------------------------------------------------------- +run_ci() { + # Print message (quiet in CI mode) + run_quietly_if_ci echo "Running tests in CI mode..." + + # Validate arguments + local test_scenario="${1}" + local efss_platform_1="${2}" + local efss_platform_2="${3}" + + if [[ -z "${test_scenario}" || -z "${efss_platform_1}" || -z "${efss_platform_2}" ]]; then + error_exit "Usage: run_ci " + fi + + # Cypress config file path + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Ensure the config file actually exists + if [[ ! -f "${cypress_config}" ]]; then + error_exit "Cypress config file not found at '${cypress_config}'." + fi + + # Adjust Cypress configurations for non-default browser platforms + if [[ "${BROWSER_PLATFORM}" != "electron" ]]; then + # Disable video and video compression + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local p1_ver="${EFSS_PLATFORM_1_VERSION%%.*}" + local p2_ver="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Construct spec file path from the arguments + local spec_path="cypress/e2e/${test_scenario}/${efss_platform_1}-${p1_ver}-to-${efss_platform_2}-${p2_ver}.cy.js" + + # Run Cypress tests in headless mode + if ! create_cypress_ci "${spec_path}"; then + error_exit "Failed to run Cypress tests with spec '${spec_path}'." + fi + + # Revert Cypress configuration changes if we modified them + if [[ "${BROWSER_PLATFORM}" != "electron" ]]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [[ -x "${ENV_ROOT}/scripts/clean.sh" ]]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi +} From 226f1bb245f0f3318db2b93c7e05f8fcbc5478f6 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Fri, 10 Jan 2025 15:54:37 +0000 Subject: [PATCH 107/184] refactor: remove functions and import them from utils --- .../share-with/nextcloud-nextcloud.sh | 404 +++----------- .../share-with/nextcloud-ocmstub.sh | 445 +++------------- .../share-with/nextcloud-owncloud.sh | 457 +++------------- .../share-with/ocmstub-nextcloud.sh | 446 +++------------- .../share-with/ocmstub-ocmstub.sh | 384 +++----------- .../share-with/ocmstub-owncloud.sh | 445 +++------------- .../share-with/owncloud-nextcloud.sh | 491 +++--------------- .../share-with/owncloud-ocmstub.sh | 445 +++------------- .../share-with/owncloud-owncloud.sh | 404 +++----------- 9 files changed, 633 insertions(+), 3288 deletions(-) diff --git a/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh index 842319c6..0471286d 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-nextcloud.sh @@ -41,281 +41,95 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v27.1.11" DEFAULT_EFSS_2_VERSION="v27.1.11" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { local source="${BASH_SOURCE[0]}" - local dir + + # Follow symbolic links until we get the real file location while [ -L "${source}" ]; do + # Get the directory path where the symlink is located dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to source="$(readlink "${source}")" - # Resolve relative symlink + # If the source was a relative symlink, convert it to an absolute path [[ "${source}" != /* ]] && source="${dir}/${source}" done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" -} - -# ----------------------------------------------------------------------------------- -# Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $@ - The command and arguments to execute. +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi -} -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag @@ -323,101 +137,11 @@ main() { create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://nextcloud2.docker (username: michiel, password: dejong)" - + run_dev \ + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://nextcloud2.docker (username: michiel, password: dejong)" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } diff --git a/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh index 0997c4c7..b3165672 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-ocmstub.sh @@ -41,306 +41,95 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v27.1.11" DEFAULT_EFSS_2_VERSION="v1.0.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- # Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_ocmstub -# Purpose: Create a OcmStub container. -# Arguments: -# $1 - Instance number. -# $2 - Image name. -# $3 - Image tag. -# ----------------------------------------------------------------------------------- -function create_ocmstub() { - local number="${1}" - local image="${2}" - local tag="${3}" - - run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" - - run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ - --name="ocmstub${number}.docker" \ - -e HOST="ocmstub${number}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 -} - - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag @@ -348,101 +137,11 @@ main() { create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://ocmstub1.docker (just click 'Log in')" - + run_dev \ + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://ocmstub1.docker (just click 'Log in')" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } diff --git a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh index 86c93d7e..3a13d272 100755 --- a/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/nextcloud-owncloud.sh @@ -41,334 +41,95 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v27.1.11" DEFAULT_EFSS_2_VERSION="v10.15.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { local source="${BASH_SOURCE[0]}" - local dir + + # Follow symbolic links until we get the real file location while [ -L "${source}" ]; do + # Get the directory path where the symlink is located dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to source="$(readlink "${source}")" - # Resolve relative symlink + # If the source was a relative symlink, convert it to an absolute path [[ "${source}" != /* ]] && source="${dir}/${source}" done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" -} -# ----------------------------------------------------------------------------------- -# Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $@ - The command and arguments to execute. +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_owncloud -# Purpose: Create a ownCloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_owncloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="mariaowncloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "mariaowncloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="owncloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="owncloud${number}" \ - -e OWNCLOUD_HOST="owncloud${number}.docker" \ - -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ - -e OWNCLOUD_ADMIN_USER="${user}" \ - -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ - -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="mariaowncloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag @@ -376,101 +137,11 @@ main() { create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://owncloud1.docker (username: marie, password: radioactivity)" - + run_dev \ + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://owncloud1.docker (username: marie, password: radioactivity)" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } diff --git a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh index f57648d8..00142ab2 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-nextcloud.sh @@ -43,408 +43,106 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v1.0.0" DEFAULT_EFSS_2_VERSION="v28.0.14" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- # Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_ocmstub -# Purpose: Create a OcmStub container. -# Arguments: -# $1 - Instance number. -# $2 - Image name. -# $3 - Image tag. -# ----------------------------------------------------------------------------------- -function create_ocmstub() { - local number="${1}" - local image="${2}" - local tag="${3}" - - run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" - - run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ - --name="ocmstub${number}.docker" \ - -e HOST="ocmstub${number}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 -} - - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_1_VERSION}" create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" - if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://ocmstub1.docker (just click 'Log in')" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - + run_dev \ + "https://ocmstub1.docker (just click 'Log in')" \ + "https://nextcloud1.docker (username: einstein, password: relativity)" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/ocmstub-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } diff --git a/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh b/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh index f604fb11..6f7b464e 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-ocmstub.sh @@ -41,244 +41,95 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v1.0.0" DEFAULT_EFSS_2_VERSION="v1.0.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- # Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Function: create_ocmstub -# Purpose: Create a OcmStub container. -# Arguments: -# $1 - Instance number. -# $2 - Image name. -# $3 - Image tag. -# ----------------------------------------------------------------------------------- -function create_ocmstub() { - local number="${1}" - local image="${2}" - local tag="${3}" - - run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" - - run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ - --name="ocmstub${number}.docker" \ - -e HOST="ocmstub${number}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 -} - - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag @@ -286,101 +137,11 @@ main() { create_ocmstub 2 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://ocmstub1.docker (just click 'Log in')" - + run_dev \ + "https://ocmstub1.docker (just click 'Log in')" \ + "https://ocmstub2.docker (just click 'Log in')" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/ocmstub-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } @@ -388,4 +149,3 @@ main() { # Execute the main function with passed arguments # ----------------------------------------------------------------------------------- main "$@" - diff --git a/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh b/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh index 2cb38083..b6ec72ca 100755 --- a/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh +++ b/dev/ocm-test-suite/share-with/ocmstub-owncloud.sh @@ -43,407 +43,106 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v1.0.0" DEFAULT_EFSS_2_VERSION="v10.15.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- # Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -# Function: create_owncloud -# Purpose: Create a ownCloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_owncloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="mariaowncloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "mariaowncloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="owncloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="owncloud${number}" \ - -e OWNCLOUD_HOST="owncloud${number}.docker" \ - -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ - -e OWNCLOUD_ADMIN_USER="${user}" \ - -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ - -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="mariaowncloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_ocmstub -# Purpose: Create a OcmStub container. -# Arguments: -# $1 - Instance number. -# $2 - Image name. -# $3 - Image tag. -# ----------------------------------------------------------------------------------- -function create_ocmstub() { - local number="${1}" - local image="${2}" - local tag="${3}" - - run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" - - run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ - --name="ocmstub${number}.docker" \ - -e HOST="ocmstub${number}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_1_VERSION}" create_owncloud 1 "einstein" "relativity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" - if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://ocmstub1.docker (just click 'Log in')" - echo " https://owncloud1.docker (username: einstein, password: relativity)" - + run_dev \ + "https://ocmstub1.docker (just click 'Log in')" \ + "https://owncloud1.docker (username: einstein, password: relativity)" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/ocmstub-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } diff --git a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh index 83cf9873..33db3284 100755 --- a/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-nextcloud.sh @@ -41,437 +41,108 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v10.15.0" DEFAULT_EFSS_2_VERSION="v27.1.11" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- # Function: initialize_environment -# Purpose: Initialize the environment and set global variables. +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_owncloud -# Purpose: Create a ownCloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_owncloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="mariaowncloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "mariaowncloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="owncloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="owncloud${number}" \ - -e OWNCLOUD_HOST="owncloud${number}.docker" \ - -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ - -e OWNCLOUD_ADMIN_USER="${user}" \ - -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ - -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="mariaowncloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { - # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi - - # Create EFSS containers - # # id # username # password # image # tag - create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" - create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" - - if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://owncloud1.docker (username: marie, password: radioactivity)" - - else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/nextcloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # username # password # image # tag + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://owncloud1.docker (username: marie, password: radioactivity)" \ + "https://nextcloud1.docker (username: einstein, password: relativity)" else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi - fi } # ----------------------------------------------------------------------------------- diff --git a/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh b/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh index 54daaa96..5569d155 100755 --- a/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-with/owncloud-ocmstub.sh @@ -41,306 +41,95 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v10.15.0" DEFAULT_EFSS_2_VERSION="v1.0.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- # Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -# Function: create_owncloud -# Purpose: Create a ownCloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_owncloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="mariaowncloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "mariaowncloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="owncloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="owncloud${number}" \ - -e OWNCLOUD_HOST="owncloud${number}.docker" \ - -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ - -e OWNCLOUD_ADMIN_USER="${user}" \ - -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ - -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="mariaowncloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_ocmstub -# Purpose: Create a OcmStub container. -# Arguments: -# $1 - Instance number. -# $2 - Image name. -# $3 - Image tag. -# ----------------------------------------------------------------------------------- -function create_ocmstub() { - local number="${1}" - local image="${2}" - local tag="${3}" - - run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" - - run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ - --name="ocmstub${number}.docker" \ - -e HOST="ocmstub${number}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 -} - - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag @@ -348,101 +137,11 @@ main() { create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://ocmstub1.docker (just click 'Log in')" - + run_dev \ + "https://owncloud1.docker (username: marie, password: radioactivity)" \ + "https://ocmstub1.docker (just click 'Log in')" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/owncloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } diff --git a/dev/ocm-test-suite/share-with/owncloud-owncloud.sh b/dev/ocm-test-suite/share-with/owncloud-owncloud.sh index 3e1c6ac7..61cec65b 100755 --- a/dev/ocm-test-suite/share-with/owncloud-owncloud.sh +++ b/dev/ocm-test-suite/share-with/owncloud-owncloud.sh @@ -41,281 +41,95 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v10.15.0" DEFAULT_EFSS_2_VERSION="v10.15.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { local source="${BASH_SOURCE[0]}" - local dir + + # Follow symbolic links until we get the real file location while [ -L "${source}" ]; do + # Get the directory path where the symlink is located dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to source="$(readlink "${source}")" - # Resolve relative symlink + # If the source was a relative symlink, convert it to an absolute path [[ "${source}" != /* ]] && source="${dir}/${source}" done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" -} - -# ----------------------------------------------------------------------------------- -# Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $@ - The command and arguments to execute. +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi -} -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: create_owncloud -# Purpose: Create a ownCloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_owncloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="mariaowncloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "mariaowncloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="owncloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="owncloud${number}" \ - -e OWNCLOUD_HOST="owncloud${number}.docker" \ - -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ - -e OWNCLOUD_ADMIN_USER="${user}" \ - -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ - -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="mariaowncloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag @@ -323,101 +137,11 @@ main() { create_owncloud 2 "mahdi" "baghbani" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://owncloud1.docker (username: marie, password: radioactivity)" - echo " https://owncloud2.docker (username: mahdi, password: baghbani)" - + run_dev \ + "https://owncloud1.docker (username: marie, password: radioactivity)" \ + "https://owncloud2.docker (username: mahdi, password: baghbani)" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/owncloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } From 6097bcf9ab28a2719d01db020f74707c08a65644 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 11 Jan 2025 09:15:25 +0000 Subject: [PATCH 108/184] fix: permission error when running apache --- docker/scripts/nextcloud/init.sh | 20 ++++++++++++++++++-- docker/scripts/owncloud/init.sh | 20 ++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docker/scripts/nextcloud/init.sh b/docker/scripts/nextcloud/init.sh index d18e2549..05896b0e 100755 --- a/docker/scripts/nextcloud/init.sh +++ b/docker/scripts/nextcloud/init.sh @@ -253,8 +253,24 @@ if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ] || [ "${NEXTCLOUD_UP # disable first run wizard run_as "php /var/www/html/console.php app:disable firstrunwizard" - # create the log file - run_as "touch /var/www/html/data/nextcloud.log" + + # create the log files + apache_log_access="/var/log/apache2/access.log" + apache_log_error="/var/log/apache2/error.log" + efss_log="/var/www/html/data/nextcloud.log" + + rm "${apache_log_access}" + rm "${apache_log_error}" + rm "${efss_log}" + + touch "${apache_log_access}" + touch "${apache_log_error}" + touch "${efss_log}" + + chown -R www-data:root /var/log/apache2 + chmod -R g=u /var/log/apache2 + chown -R www-data:root /var/www/html/data + chmod -R g=u /var/www/html/data run_path post-installation fi diff --git a/docker/scripts/owncloud/init.sh b/docker/scripts/owncloud/init.sh index 5e768ba5..22f02ca5 100755 --- a/docker/scripts/owncloud/init.sh +++ b/docker/scripts/owncloud/init.sh @@ -252,8 +252,24 @@ if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ] || [ "${OWNCLOUD_UPD # disable first run wizard run_as "php /var/www/html/console.php app:disable firstrunwizard" - # create the log file - run_as "touch /var/www/html/data/owncloud.log" + + # create the log files + apache_log_access="/var/log/apache2/access.log" + apache_log_error="/var/log/apache2/error.log" + efss_log="/var/www/html/data/owncloud.log" + + rm "${apache_log_access}" + rm "${apache_log_error}" + rm "${efss_log}" + + touch "${apache_log_access}" + touch "${apache_log_error}" + touch "${efss_log}" + + chown -R www-data:root /var/log/apache2 + chmod -R g=u /var/log/apache2 + chown -R www-data:root /var/www/html/data + chmod -R g=u /var/www/html/data run_as "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" From 6eebb506ab8883e625c27c7986976e2f55238000 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 11 Jan 2025 13:13:31 +0000 Subject: [PATCH 109/184] fix: owncloud link generation --- .../share-link/owncloud-v10-to-nextcloud-v27.cy.js | 11 ++--------- .../share-link/owncloud-v10-to-nextcloud-v28.cy.js | 11 ++--------- .../share-link/owncloud-v10-to-owncloud-v10.cy.js | 13 +++---------- .../ocm-test-suite/cypress/e2e/utils/owncloud.js | 8 +++++--- 4 files changed, 12 insertions(+), 31 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js index c0b69786..ce08b5ee 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js @@ -14,14 +14,7 @@ describe('Share link federated sharing functionality for ownCloud', () => { cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') renameFile('welcome.txt', 'oc1-to-nc1-share-link.txt') - createShareLink('oc1-to-nc1-share-link.txt').then( - (result) => { - cy.visit(result) - - // save share url to file. - cy.writeFile('share-link-url.txt', result) - } - ) + createShareLink('oc1-to-nc1-share-link.txt') }) it('Receive federated share from ownCloud v10 to Nextcloud v27', () => { @@ -30,7 +23,7 @@ describe('Share link federated sharing functionality for ownCloud', () => { cy.readFile('share-link-url.txt').then((result) => { // extract token from url. - const token = result.replace('https://owncloud1.docker/index.php/s/',''); + const token = result.replace('https://owncloud1.docker/s/',''); // put token into the link. const url = `https://nextcloud1.docker/index.php/login?redirect_url=%252Findex.php%252Fapps%252Ffiles#remote=https%3A%2F%2Fowncloud1.docker&token=${token}&owner=marie&ownerDisplayName=marie&name=oc1-to-oc2-share-link.txt&protected=0` diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js index a47d8aa1..041ce36b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js @@ -6,14 +6,7 @@ describe('Share link federated sharing functionality for ownCloud', () => { cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') renameFile('welcome.txt', 'oc1-to-nc1-share-link.txt') - createShareLink('oc1-to-nc1-share-link.txt').then( - (result) => { - cy.visit(result) - - // save share url to file. - cy.writeFile('share-link-url.txt', result) - } - ) + createShareLink('oc1-to-nc1-share-link.txt') }) it('Receive federated share from ownCloud v10 to Nextcloud v28', () => { @@ -22,7 +15,7 @@ describe('Share link federated sharing functionality for ownCloud', () => { cy.readFile('share-link-url.txt').then((result) => { // extract token from url. - const token = result.replace('https://owncloud1.docker/index.php/s/',''); + const token = result.replace('https://owncloud1.docker/s/',''); // put token into the link. const url = `https://nextcloud1.docker/index.php/login?redirect_url=%252Findex.php%252Fapps%252Ffiles#remote=https%3A%2F%2Fowncloud1.docker&token=${token}&owner=marie&ownerDisplayName=marie&name=oc1-to-oc2-share-link.txt&protected=0` diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js index f4f6d620..8af95359 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js @@ -10,14 +10,7 @@ describe('Share link federated sharing functionality for ownCloud', () => { cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') renameFile('welcome.txt', 'oc1-to-oc2-share-link.txt') - createShareLink('oc1-to-oc2-share-link.txt').then( - (result) => { - cy.visit(result) - - // save share url to file. - cy.writeFile('share-link-url.txt', result) - } - ) + createShareLink('oc1-to-oc2-share-link.txt') }) it('Receive federated share from ownCloud v10 to ownCloud v10', () => { @@ -26,10 +19,10 @@ describe('Share link federated sharing functionality for ownCloud', () => { cy.readFile('share-link-url.txt').then((result) => { // extract token from url. - const token = result.replace('https://owncloud1.docker/index.php/s/',''); + const token = result.replace('https://owncloud1.docker/s/',''); // put token into the link. - const url = `https://owncloud2.docker/index.php/login?redirect_url=%252Findex.php%252Fapps%252Ffiles#remote=https%3A%2F%2Fowncloud1.docker&token=${token}&owner=marie&ownerDisplayName=marie&name=oc1-to-oc2-share-link.txt&protected=0` + const url = `https://owncloud2.docker/index.php/apps/files#remote=https%3A%2F%2Fowncloud1.docker&token=${token}&owner=marie&ownerDisplayName=marie&name=oc1-to-oc2-share-link.txt&protected=0` // accept share from ownCloud 2. cy.loginOwncloudCore(url, 'mahdi', 'baghbani') diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index 109a71ed..b38b041b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -149,11 +149,13 @@ export function createShareLink(fileName) { }); // Extract and return the public share link - return cy.get('#app-sidebar').within(() => { - return cy.get('.shareLink input') + cy.get('#app-sidebar').within(() => { + cy.get('.link-entry .linkText') .invoke('val') .then((link) => { - return link; + cy.visit(link) + // save share url to file. + cy.writeFile('share-link-url.txt', link) }); }); } From 6ee5b9143273611cb294b9f244e74dcb7d43fd49 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 11 Jan 2025 13:14:00 +0000 Subject: [PATCH 110/184] fix: nc to os test cypress import module --- .../cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js index bce16036..9c9c4d78 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-ocmstub-v1.cy.js @@ -8,6 +8,7 @@ import { createShareLinkV27, + ensureFileExistsV27, renameFileV27, } from '../utils/nextcloud-v27'; From d4caf38cf367d0eefbc6be256540a3bf98249739 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 11 Jan 2025 13:14:23 +0000 Subject: [PATCH 111/184] fix: docker push login --- docker/push/all.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docker/push/all.sh b/docker/push/all.sh index 2c7d31e9..f28213e9 100755 --- a/docker/push/all.sh +++ b/docker/push/all.sh @@ -59,7 +59,7 @@ parse_arguments "$@" # Parse any arguments passed to the script. # Ensure successful login to Docker before pushing images. echo "Logging in to Docker as pondersource..." -if ! docker login; then +if ! docker login -u pondersource; then echo "Docker login failed. Exiting." exit 1 fi @@ -71,8 +71,12 @@ run_quietly_if_ci echo "Pushing PonderSource-specific Docker images..." # Push the core PonderSource images. run_quietly_if_ci docker push pondersource/revad:latest -run_quietly_if_ci docker push pondersource/ocmstub:latest -run_quietly_if_ci docker push pondersource/ocmstub:v1.0.0 + +# OcmStub: push multiple versions of the OcmStub Docker image. +ocmstub_versions=("latest" "v1.0.0") +for version in "${ocmstub_versions[@]}"; do + run_quietly_if_ci docker push "pondersource/ocmstub:${version}" +done # Nextcloud: push multiple versions of the Nextcloud Docker image. run_quietly_if_ci docker push pondersource/nextcloud-base:latest From 07005a85ecfac3ea8b13bd33a96cc07f1c0a3e9b Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Sat, 11 Jan 2025 13:14:49 +0000 Subject: [PATCH 112/184] refactor: scripts to include utils.sh --- .../share-link/nextcloud-nextcloud.sh | 407 +++------------- .../share-link/nextcloud-ocmstub.sh | 449 +++--------------- .../share-link/nextcloud-owncloud.sh | 384 ++++++--------- .../share-link/owncloud-nextcloud.sh | 381 ++++++--------- .../share-link/owncloud-owncloud.sh | 377 ++++++--------- 5 files changed, 586 insertions(+), 1412 deletions(-) diff --git a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh index 010e5875..439ca035 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-nextcloud.sh @@ -43,281 +43,95 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v27.1.11" DEFAULT_EFSS_2_VERSION="v27.1.11" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { local source="${BASH_SOURCE[0]}" - local dir + + # Follow symbolic links until we get the real file location while [ -L "${source}" ]; do + # Get the directory path where the symlink is located dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to source="$(readlink "${source}")" - # Resolve relative symlink + # If the source was a relative symlink, convert it to an absolute path [[ "${source}" != /* ]] && source="${dir}/${source}" done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" -} - -# ----------------------------------------------------------------------------------- -# Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $@ - The command and arguments to execute. +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi -} -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag @@ -325,104 +139,15 @@ main() { create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" # disable cypress editing javascript files. it would make adding share to your own efss fail. - sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' \ + "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://nextcloud2.docker (username: michiel, password: dejong)" - + run_dev \ + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://nextcloud2.docker (username: michiel, password: dejong)" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } diff --git a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh index 3874ba4b..ee9cd338 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-ocmstub.sh @@ -43,306 +43,95 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v27.1.11" DEFAULT_EFSS_2_VERSION="v1.0.0" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { - local source="${BASH_SOURCE[0]}" - local dir - while [ -L "${source}" ]; do - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - source="$(readlink "${source}")" - # Resolve relative symlink - [[ "${source}" != /* ]] && source="${dir}/${source}" - done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- # Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_ocmstub -# Purpose: Create a OcmStub container. -# Arguments: -# $1 - Instance number. -# $2 - Image name. -# $3 - Image tag. -# ----------------------------------------------------------------------------------- -function create_ocmstub() { - local number="${1}" - local image="${2}" - local tag="${3}" - - run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" - - run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ - --name="ocmstub${number}.docker" \ - -e HOST="ocmstub${number}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 -} - - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } # ----------------------------------------------------------------------------------- # Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. # ----------------------------------------------------------------------------------- main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi + initialize_environment "../../.." + setup "$@" # Create EFSS containers # # id # username # password # image # tag @@ -352,102 +141,16 @@ main() { # disable cypress editing javascript files. it would make adding share to your own efss fail. sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://ocmstub1.docker (just click 'Log in')" + # disable cypress editing javascript files. it would make adding share to your own efss fail. + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' \ + "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://ocmstub1.docker (just click 'Log in')" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-ocmstub-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } diff --git a/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh b/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh index 91518da9..09d9be1e 100755 --- a/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh +++ b/dev/ocm-test-suite/share-link/nextcloud-owncloud.sh @@ -1,245 +1,157 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_2_VERSION=${2:-"v10.15.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to ownCloud OCM share-link flow tests. +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, ownCloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v10.15.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-owncloud.sh v28.0.14 v10.15.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v10.15.0" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sm-ocm.sh" "${ENV_ROOT}/temp/owncloud.sh" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -############ -### EFSS ### -############ - -# syntax: -# createEfss platform number username password image. +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE # +# Arguments: +# All command line arguments are passed to parse_arguments. # -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownCloud. -createEfss owncloud 1 marie radioactivity owncloud.sh latest ocm-test-suite - -# Nextcloud. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_1_VERSION}" - -# disable cypress editing javascript files. it would make adding share to your own efss fail. -sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # for some reason this test fails on ci! so lets sleep a bit. - sleep 60 - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/nextcloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + # disable cypress editing javascript files. it would make adding share to your own efss fail. + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' \ + "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://owncloud1.docker (username: marie, password: radioactivity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh b/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh index 735d31c8..b36648d9 100755 --- a/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh +++ b/dev/ocm-test-suite/share-link/owncloud-nextcloud.sh @@ -1,242 +1,157 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_2_VERSION=${2:-"v28.0.14"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi +# ----------------------------------------------------------------------------------- +# Script to Test ownCloud to Nextcloud OCM share-link flow tests. +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, ownCloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./owncloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v10.15.0"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./owncloud-nextcloud.sh v10.15.0 v28.0.14 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v10.15.0" +DEFAULT_EFSS_2_VERSION="v27.1.11" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sm-ocm.sh" "${ENV_ROOT}/temp/owncloud.sh" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -############ -### EFSS ### -############ - -# syntax: -# createEfss platform number username password image. +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE # +# Arguments: +# All command line arguments are passed to parse_arguments. # -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownCloud. -createEfss owncloud 1 marie radioactivity owncloud.sh latest ocm-test-suite - -# Nextcloud. -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_2_VERSION}" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # for some reason this test fails on ci! so lets sleep a bit. - sleep 60 - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/owncloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # username # password # image # tag + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + + # disable cypress editing javascript files. it would make adding share to your own efss fail. + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' \ + "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://owncloud1.docker (username: marie, password: radioactivity)" \ + "https://nextcloud1.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/share-link/owncloud-owncloud.sh b/dev/ocm-test-suite/share-link/owncloud-owncloud.sh index 684aad65..5027bf1b 100755 --- a/dev/ocm-test-suite/share-link/owncloud-owncloud.sh +++ b/dev/ocm-test-suite/share-link/owncloud-owncloud.sh @@ -1,238 +1,157 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_2_VERSION=${2:-"v10.15.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi +# ----------------------------------------------------------------------------------- +# Script to Test ownCloud to ownCloud OCM share-link flow tests. +# Authors: +# 1. Michiel B. de Jong +# 2. Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, ownCloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./owncloud-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v10.15.0"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./owncloud-owncloud.sh v10.15.0 v28.0.14 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v10.15.0" +DEFAULT_EFSS_2_VERSION="v10.15.0" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sm-ocm.sh" "${ENV_ROOT}/temp/owncloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################ -### ownCloud ### -################ - -# syntax: -# createEfss platform number username password image. +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE # +# Arguments: +# All command line arguments are passed to parse_arguments. # -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownClouds. -createEfss owncloud 1 marie radioactivity owncloud.sh latest ocm-test-suite -createEfss owncloud 2 mahdi baghbani owncloud.sh latest ocm-test-suite - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://nextcloud2.docker -> username: michiel password: dejong" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # for some reason this test fails on ci! so lets sleep a bit. - sleep 60 - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-link/owncloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # username # password # image # tag + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 2 "mahdi" "baghbani" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + # disable cypress editing javascript files. it would make adding share to your own efss fail. + sed -i 's/.*modifyObstructiveCode: true,.*/ modifyObstructiveCode: false,/' \ + "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://owncloud1.docker (username: marie, password: radioactivity)" \ + "https://owncloud2.docker (username: mahdi, password: baghbani)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From defc8272b62af5c59cd0a5b07d8f0e9c5db78bbf Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 20 Jan 2025 13:44:39 +0330 Subject: [PATCH 113/184] add: modular util files for better DX --- scripts/utils.sh | 725 ++------------------------- scripts/utils/constants.sh | 29 ++ scripts/utils/container/cypress.sh | 38 ++ scripts/utils/container/firefox.sh | 18 + scripts/utils/container/nextcloud.sh | 45 ++ scripts/utils/container/owncloud.sh | 45 ++ scripts/utils/container/vnc.sh | 26 + scripts/utils/docker.sh | 25 + scripts/utils/environment.sh | 43 ++ scripts/utils/errors.sh | 22 + scripts/utils/test_modes/ci.sh | 57 +++ scripts/utils/test_modes/dev.sh | 35 ++ scripts/utils/validation.sh | 38 ++ 13 files changed, 455 insertions(+), 691 deletions(-) create mode 100644 scripts/utils/constants.sh create mode 100644 scripts/utils/container/cypress.sh create mode 100644 scripts/utils/container/firefox.sh create mode 100644 scripts/utils/container/nextcloud.sh create mode 100644 scripts/utils/container/owncloud.sh create mode 100644 scripts/utils/container/vnc.sh create mode 100644 scripts/utils/docker.sh create mode 100644 scripts/utils/environment.sh create mode 100644 scripts/utils/errors.sh create mode 100644 scripts/utils/test_modes/ci.sh create mode 100644 scripts/utils/test_modes/dev.sh create mode 100644 scripts/utils/validation.sh diff --git a/scripts/utils.sh b/scripts/utils.sh index 87a627db..0504ea1d 100755 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -1,707 +1,50 @@ #!/usr/bin/env bash # ----------------------------------------------------------------------------------- -# Script to hold all the utility functions needed in the Dev Stock. +# Main utility script that sources all modular components # Author: Mohammad Mahdi Baghbani Pourvahid # ----------------------------------------------------------------------------------- -# Exit immediately if a command exits with a non-zero status, -# a variable is used but not defined, or a command in a pipeline fails +# Exit on error, undefined vars, and pipe failures set -euo pipefail -# ----------------------------------------------------------------------------------- -# Constants and Default Values -# ----------------------------------------------------------------------------------- - -# Default versions -DEFAULT_EFSS_1_VERSION="${1}" -DEFAULT_EFSS_2_VERSION="${2}" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" -export DEFAULT_EFSS_1_VERSION="${DEFAULT_EFSS_1_VERSION}" -export DEFAULT_EFSS_2_VERSION="${DEFAULT_EFSS_2_VERSION}" -export DEFAULT_SCRIPT_MODE="${DEFAULT_SCRIPT_MODE}" -export DEFAULT_BROWSER_PLATFORM="${DEFAULT_BROWSER_PLATFORM}" - -# Docker network name -DOCKER_NETWORK="testnet" -export DOCKER_NETWORK="${DOCKER_NETWORK}" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" -export MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" -export TEMP_DIR="${TEMP_DIR}" -export TLS_CA_DIR="${TLS_CA_DIR}" -export TLS_CERTIFICATES_DIR="${TLS_CERTIFICATES_DIR}" - -# 3rd party containers -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1 -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1 -MARIADB_REPO=mariadb -MARIADB_TAG=11.4.4 -VNC_REPO=theasp/novnc -VNC_TAG=latest -export CYPRESS_REPO="${CYPRESS_REPO}" -export CYPRESS_TAG="${CYPRESS_TAG}" -export FIREFOX_REPO="${FIREFOX_REPO}" -export FIREFOX_TAG="${FIREFOX_TAG}" -export MARIADB_REPO="${MARIADB_REPO}" -export MARIADB_TAG="${MARIADB_TAG}" -export VNC_REPO="${VNC_REPO}" -export VNC_TAG="${VNC_TAG}" - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. -# Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. -# ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} +# Store the directory of utils.sh +UTILS_DIR="${ENV_ROOT}/scripts" +MODULES_DIR="${UTILS_DIR}/utils" -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +# Function to source a module +source_module() { + local module="$1" + if [[ -f "${MODULES_DIR}/${module}" ]]; then + # shellcheck source=/dev/null + source "${MODULES_DIR}/${module}" else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + echo "Error: Module '${module}' not found" >&2 + exit 1 fi } -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Function: ensure_required_commands -# Purpose : Ensure that certain commands (e.g., docker) are available on the system. -# Arguments: (none) -# Returns : -# Calls error_exit if any required command is missing. -# ----------------------------------------------------------------------------------- -ensure_required_commands() { - # Ensure required commands are available (here, just 'docker') - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: ensure_docker_running -# Purpose : -# 1) Verify that the Docker daemon is running and accessible (e.g., user permissions). -# -# Arguments: -# (none) -# -# Returns : -# Exits with an error if Docker is either not installed or not running. -# ----------------------------------------------------------------------------------- -ensure_docker_running() { - # Check if the Docker daemon is running (or user has permission) - # 'docker info' returns non-zero if the daemon is not reachable. - if ! docker info >/dev/null 2>&1; then - error_exit "Cannot connect to the Docker daemon. Is it running and do you have the right permissions?" - fi -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: extract_platform_variables -# Purpose : -# 1) Use the script's own file path to determine: -# - TEST_SCENARIO (from its parent directory) -# - EFSS_PLATFORM_1 and EFSS_PLATFORM_2 (from the file name minus .sh) -# 2) Export the resulting variables for use in the rest of the script. -# -# Arguments: -# (none) -# Returns : -# Exports TEST_SCENARIO, EFSS_PLATFORM_1, EFSS_PLATFORM_2 -# ----------------------------------------------------------------------------------- -extract_platform_variables() { - # Extract the parent folder name as TEST_SCENARIO - local test_scenario - test_scenario="$(basename "$(dirname "${SOURCE}")")" - - # Extract the filename without the .sh extension - local filename - filename="$(basename "${SOURCE}" .sh)" - - # Split the filename by '-' to get EFSS_PLATFORM_1 and EFSS_PLATFORM_2 - local platform1 platform2 - IFS='-' read -r platform1 platform2 <<< "${filename}" - - # Export the variables so the rest of the script can use them - export TEST_SCENARIO="${test_scenario}" - export EFSS_PLATFORM_1="${platform1}" - export EFSS_PLATFORM_2="${platform2}" -} - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" - - export EFSS_PLATFORM_1_VERSION="${EFSS_PLATFORM_1_VERSION}" - export EFSS_PLATFORM_2_VERSION="${EFSS_PLATFORM_2_VERSION}" - export SCRIPT_MODE="${SCRIPT_MODE}" - export BROWSER_PLATFORM="${BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: prepare_environment -# Purpose: 1) Prepare temporary directories and copy necessary files -# 2) Run cleanup script (if it exists) -# 3) Ensure the specified Docker network is available -# ----------------------------------------------------------------------------------- -prepare_environment() { - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - - # Clean up previous resources (if the cleanup script is available) - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - # Ensure Docker network exists - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || - error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi -} - -# ----------------------------------------------------------------------------------- -# Function: setup_initial_environment -# Purpose : -# 1) Extract platform-dependent variables from the script path. -# 2) Ensure required commands (and Docker daemon) are running. -# 3) Parse command-line arguments. -# 4) Validate necessary files. -# 5) Prepare the environment (e.g., set up networks, clean old resources). -# -# Arguments: -# * - All arguments passed to the script (forwarded to parse_arguments). -# -# Returns : None. Exits on error if a required step fails. -# ----------------------------------------------------------------------------------- -setup() { - # Get platform dependent variables. - extract_platform_variables - - # Ensure required commands (including Docker) are available. - ensure_required_commands - ensure_docker_running +# Source all base modules +source_module "constants.sh" +source_module "errors.sh" +source_module "environment.sh" +source_module "docker.sh" +source_module "validation.sh" - # Parse CLI arguments - parse_arguments "$@" +# Source container modules +for module in "${MODULES_DIR}/container"/*.sh; do + # shellcheck source=/dev/null + source "$module" +done - # Validate required files/directories - validate_files +# Source test mode modules +for module in "${MODULES_DIR}/test_modes"/*.sh; do + # shellcheck source=/dev/null + source "$module" +done - # Prepare the environment - prepare_environment -} - -# ----------------------------------------------------------------------------------- -# Function: print_ocm_test_setup_instructions -# Purpose : Print messages indicating that the development environment is ready, -# along with URLs and usage notes. -# Arguments: (none) -# Returns : (none) -# ----------------------------------------------------------------------------------- -print_ocm_test_setup_instructions() { - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" -} - -# ----------------------------------------------------------------------------------- -# Function: create_firefox -# Purpose : Launch a Firefox container with the necessary environment variables, -# volume mounts, and network configuration. -# Arguments: (none) -# Returns : (none) -# -# Example Usage: -# create_firefox -# ----------------------------------------------------------------------------------- -create_firefox() { - # Print message (quiet in CI mode) - run_quietly_if_ci echo "Starting Firefox container..." - - # Run Docker container with the specified parameters - run_docker_container --detach \ - --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}:${FIREFOX_TAG}" || error_exit "Failed to start Firefox." -} - -# ----------------------------------------------------------------------------------- -# Function: create_vnc -# Purpose : Launch a VNC server container with the necessary environment variables, -# volume mounts, and network configuration. -# Arguments: (none) -# Returns : (none) -# -# Example Usage: -# create_vnc -# ----------------------------------------------------------------------------------- -create_vnc() { - # Print message (quiet in CI mode) - run_quietly_if_ci echo "Starting VNC Server..." - - # Define path to the X11 socket directory - X11_SOCKET="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - export X11_SOCKET="${X11_SOCKET}" - - # Clean up any previous socket files and create a new directory - remove_directory "${X11_SOCKET}" - mkdir -p "${X11_SOCKET}" - - # Launch the VNC server container - run_docker_container --detach \ - --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${X11_SOCKET}:/tmp/.X11-unix" \ - "${VNC_REPO}:${VNC_TAG}" || error_exit "Failed to start VNC Server." -} - -# ----------------------------------------------------------------------------------- -# Function: create_cypress_dev -# Purpose : Launch a Cypress container with the necessary environment variables, -# volume mounts, and network configuration. -# Arguments: (none) -# Returns : (none) -# -# Requirements: -# - Environment vars: DOCKER_NETWORK, ENV_ROOT, X11_SOCKET, CYPRESS_REPO, CYPRESS_TAG -# - External function: run_docker_container -# -# Example Usage: -# create_cypress_dev -# ----------------------------------------------------------------------------------- -create_cypress_dev() { - # Print message (quiet in CI mode) - run_quietly_if_ci echo "Starting Cypress container..." - - run_docker_container --detach \ - --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${X11_SOCKET}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}:${CYPRESS_TAG}" \ - open --project . || error_exit "Failed to start Cypress." -} - -# ----------------------------------------------------------------------------------- -# Function: create_cypress_ci -# Purpose : Run Cypress tests in headless mode with the specified parameters. -# Arguments: -# 1) ${1} - The Cypress spec path (relative path to the spec file). -# Returns : (none) - exits on error -# -# Usage Example: -# create_cypress_ci "cypress/e2e/share-with/nextcloud-v27-to-nextcloud-v28.cy.js" -# ----------------------------------------------------------------------------------- -create_cypress_ci() { - local cypress_spec="${1}" - - if [[ -z "$cypress_spec" ]]; then - error_exit "No Cypress spec provided. Usage: create_cypress_ci " - fi - - # Print message (quiet in CI mode) - run_quietly_if_ci echo "Running Cypress tests using spec: $cypress_spec" - - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}:${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "${cypress_spec}" || - error_exit "Cypress tests failed." -} - -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ - -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ - -e NEXTCLOUD_ADMIN_USER="${user}" \ - -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ - -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="marianextcloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_owncloud -# Purpose: Create a ownCloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Image name. -# $5 - Image tag. -# ----------------------------------------------------------------------------------- -create_owncloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local image="${4}" - local tag="${5}" - - run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="mariaowncloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --log-bin=binlog \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "mariaowncloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="owncloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="owncloud${number}" \ - -e OWNCLOUD_HOST="owncloud${number}.docker" \ - -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ - -e OWNCLOUD_ADMIN_USER="${user}" \ - -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ - -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ - -e MYSQL_HOST="mariaowncloud${number}.docker" \ - -e MYSQL_DATABASE="efss" \ - -e MYSQL_USER="root" \ - -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: create_ocmstub -# Purpose: Create a OcmStub container. -# Arguments: -# $1 - Instance number. -# $2 - Image name. -# $3 - Image tag. -# ----------------------------------------------------------------------------------- -function create_ocmstub() { - local number="${1}" - local image="${2}" - local tag="${3}" - - run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" - - run_quietly_if_ci docker run --detach --network="${DOCKER_NETWORK}" \ - --name="ocmstub${number}.docker" \ - -e HOST="ocmstub${number}" \ - "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 -} - -# ----------------------------------------------------------------------------------- -# Function: run_dev -# Purpose : -# 1) Quietly log environment setup when in CI. -# 2) Create Firefox, VNC, and Cypress containers in dev mode. -# 3) Print OCM test setup instructions. -# 4) Echo two additional lines (supplied as arguments), e.g. EFSS login URLs. -# -# Arguments: -# 1) $1 - The first line to echo (e.g., "https://nextcloud1.docker (username...)") -# 2) $2 - The second line to echo (e.g., "https://nextcloud2.docker (username...)") -# -# Example Usage: -# run_dev \ -# "https://nextcloud1.docker (username: einstein, password: relativity)" \ -# "https://nextcloud2.docker (username: michiel, password: dejong)" -# -# Returns : None -# ----------------------------------------------------------------------------------- -run_dev() { - local url_line_1="${1}" - local url_line_2="${2}" - - # Quiet log in CI mode - run_quietly_if_ci echo "Setting up development environment..." - - # Create containers for Firefox, VNC, and Cypress (dev mode) - create_firefox - create_vnc - create_cypress_dev - - # Display setup instructions - print_ocm_test_setup_instructions - - # Echo the two lines passed as arguments - echo " ${url_line_1}" - echo " ${url_line_2}" -} - -# ----------------------------------------------------------------------------------- -# Function: run_ci -# Purpose : -# 1) Update Cypress config based on the chosen browser platform (disables video -# unless the BROWSER_PLATFORM is "electron"). -# 2) Compute major EFSS platform version numbers and run Cypress tests headlessly, -# using a test scenario path that is dynamically formed. -# 3) Revert Cypress config changes and perform a cleanup of the environment. -# -# Arguments: -# 1) $1 - The test scenario folder name (sub-path under cypress/e2e/). -# 2) $2 - The EFSS platform 1 name (e.g., "nextcloud"). -# 3) $3 - The EFSS platform 2 name (e.g., "nextcloud", "owncloud", etc.). -# -# Returns : -# None. Exits (via error_exit) on critical failure. -# ----------------------------------------------------------------------------------- -run_ci() { - # Print message (quiet in CI mode) - run_quietly_if_ci echo "Running tests in CI mode..." - - # Validate arguments - local test_scenario="${1}" - local efss_platform_1="${2}" - local efss_platform_2="${3}" - - if [[ -z "${test_scenario}" || -z "${efss_platform_1}" || -z "${efss_platform_2}" ]]; then - error_exit "Usage: run_ci " - fi - - # Cypress config file path - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Ensure the config file actually exists - if [[ ! -f "${cypress_config}" ]]; then - error_exit "Cypress config file not found at '${cypress_config}'." - fi - - # Adjust Cypress configurations for non-default browser platforms - if [[ "${BROWSER_PLATFORM}" != "electron" ]]; then - # Disable video and video compression - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local p1_ver="${EFSS_PLATFORM_1_VERSION%%.*}" - local p2_ver="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Construct spec file path from the arguments - local spec_path="cypress/e2e/${test_scenario}/${efss_platform_1}-${p1_ver}-to-${efss_platform_2}-${p2_ver}.cy.js" - - # Run Cypress tests in headless mode - if ! create_cypress_ci "${spec_path}"; then - error_exit "Failed to run Cypress tests with spec '${spec_path}'." - fi - - # Revert Cypress configuration changes if we modified them - if [[ "${BROWSER_PLATFORM}" != "electron" ]]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [[ -x "${ENV_ROOT}/scripts/clean.sh" ]]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi -} +# Export the version variables passed from the main script +DEFAULT_EFSS_1_VERSION="${1}" +DEFAULT_EFSS_2_VERSION="${2}" +export DEFAULT_EFSS_1_VERSION +export DEFAULT_EFSS_2_VERSION diff --git a/scripts/utils/constants.sh b/scripts/utils/constants.sh new file mode 100644 index 00000000..4b12a794 --- /dev/null +++ b/scripts/utils/constants.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# Docker network name +DOCKER_NETWORK="testnet" +export DOCKER_NETWORK + +# MariaDB root password +MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" +export MARIADB_ROOT_PASSWORD + +# Paths to required directories +TEMP_DIR="temp" +TLS_CA_DIR="docker/tls/certificate-authority" +TLS_CERTIFICATES_DIR="docker/tls/certificates" +export TEMP_DIR TLS_CA_DIR TLS_CERTIFICATES_DIR + +# 3rd party containers +CYPRESS_REPO=cypress/included +CYPRESS_TAG=13.13.1 +FIREFOX_REPO=jlesage/firefox +FIREFOX_TAG=v24.11.1 +MARIADB_REPO=mariadb +MARIADB_TAG=11.4.4 +VNC_REPO=theasp/novnc +VNC_TAG=latest + +# Export the constants +export CYPRESS_REPO CYPRESS_TAG FIREFOX_REPO FIREFOX_TAG +export MARIADB_REPO MARIADB_TAG VNC_REPO VNC_TAG diff --git a/scripts/utils/container/cypress.sh b/scripts/utils/container/cypress.sh new file mode 100644 index 00000000..bdcf73e4 --- /dev/null +++ b/scripts/utils/container/cypress.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Create Cypress container for development mode +create_cypress_dev() { + run_quietly_if_ci echo "Starting Cypress container..." + + run_docker_container --detach \ + --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -e DISPLAY="vnc-server:0.0" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -v "${X11_SOCKET}:/tmp/.X11-unix" \ + -w /ocm \ + --entrypoint cypress \ + "${CYPRESS_REPO}:${CYPRESS_TAG}" \ + open --project . || error_exit "Failed to start Cypress." +} + +# Create Cypress container for CI mode +create_cypress_ci() { + local cypress_spec="${1}" + + if [[ -z "$cypress_spec" ]]; then + error_exit "No Cypress spec provided. Usage: create_cypress_ci " + fi + + run_quietly_if_ci echo "Running Cypress tests using spec: $cypress_spec" + + docker run --network="${DOCKER_NETWORK}" \ + --name="cypress.docker" \ + -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ + -w /ocm \ + "${CYPRESS_REPO}:${CYPRESS_TAG}" \ + cypress run \ + --browser "${BROWSER_PLATFORM}" \ + --spec "${cypress_spec}" || + error_exit "Cypress tests failed." +} diff --git a/scripts/utils/container/firefox.sh b/scripts/utils/container/firefox.sh new file mode 100644 index 00000000..7d61bbd6 --- /dev/null +++ b/scripts/utils/container/firefox.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Create Firefox container +create_firefox() { + run_quietly_if_ci echo "Starting Firefox container..." + + run_docker_container --detach \ + --network="${DOCKER_NETWORK}" \ + --name="firefox" \ + -p 5800:5800 \ + --shm-size=2g \ + -e USER_ID="$(id -u)" \ + -e GROUP_ID="$(id -g)" \ + -e DARK_MODE=1 \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ + -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ + "${FIREFOX_REPO}:${FIREFOX_TAG}" || error_exit "Failed to start Firefox." +} diff --git a/scripts/utils/container/nextcloud.sh b/scripts/utils/container/nextcloud.sh new file mode 100644 index 00000000..13060339 --- /dev/null +++ b/scripts/utils/container/nextcloud.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Create a Nextcloud container with MariaDB backend +create_nextcloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="marianextcloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "marianextcloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="nextcloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="nextcloud${number}" \ + -e NEXTCLOUD_HOST="nextcloud${number}.docker" \ + -e NEXTCLOUD_TRUSTED_DOMAINS="nextcloud${number}.docker" \ + -e NEXTCLOUD_ADMIN_USER="${user}" \ + -e NEXTCLOUD_ADMIN_PASSWORD="${password}" \ + -e NEXTCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="marianextcloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for nextcloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 +} diff --git a/scripts/utils/container/owncloud.sh b/scripts/utils/container/owncloud.sh new file mode 100644 index 00000000..ea31a180 --- /dev/null +++ b/scripts/utils/container/owncloud.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Create an ownCloud container with MariaDB backend +create_owncloud() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + + run_quietly_if_ci echo "Creating EFSS instance: owncloud ${number}" + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaowncloud${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}":"${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for owncloud ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaowncloud${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="owncloud${number}.docker" \ + --add-host "host.docker.internal:host-gateway" \ + -e HOST="owncloud${number}" \ + -e OWNCLOUD_HOST="owncloud${number}.docker" \ + -e OWNCLOUD_TRUSTED_DOMAINS="owncloud${number}.docker" \ + -e OWNCLOUD_ADMIN_USER="${user}" \ + -e OWNCLOUD_ADMIN_PASSWORD="${password}" \ + -e OWNCLOUD_APACHE_LOGLEVEL="warn" \ + -e MYSQL_HOST="mariaowncloud${number}.docker" \ + -e MYSQL_DATABASE="efss" \ + -e MYSQL_USER="root" \ + -e MYSQL_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for owncloud ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "owncloud${number}.docker" 443 +} diff --git a/scripts/utils/container/vnc.sh b/scripts/utils/container/vnc.sh new file mode 100644 index 00000000..18909152 --- /dev/null +++ b/scripts/utils/container/vnc.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Create VNC container +create_vnc() { + run_quietly_if_ci echo "Starting VNC Server..." + + # Define path to the X11 socket directory + X11_SOCKET="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" + export X11_SOCKET="${X11_SOCKET}" + + # Clean up any previous socket files and create a new directory + remove_directory "${X11_SOCKET}" + mkdir -p "${X11_SOCKET}" + + # Launch the VNC server container + run_docker_container --detach \ + --network="${DOCKER_NETWORK}" \ + --name="vnc-server" \ + -p 5700:8080 \ + -e RUN_XTERM=no \ + -e DISPLAY_WIDTH=1920 \ + -e DISPLAY_HEIGHT=1080 \ + -v "${X11_SOCKET}:/tmp/.X11-unix" \ + "${VNC_REPO}:${VNC_TAG}" || error_exit "Failed to start VNC Server." +} + \ No newline at end of file diff --git a/scripts/utils/docker.sh b/scripts/utils/docker.sh new file mode 100644 index 00000000..7b2cdfa5 --- /dev/null +++ b/scripts/utils/docker.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Run a Docker container with provided arguments +run_docker_container() { + run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" +} + +# Prepare Docker environment (network, cleanup) +prepare_environment() { + # Prepare temporary directories and copy necessary files + remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + + # Clean up previous resources (if the cleanup script is available) + if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." + fi + + # Ensure Docker network exists + if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then + docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || + error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." + fi +} diff --git a/scripts/utils/environment.sh b/scripts/utils/environment.sh new file mode 100644 index 00000000..c6e4df65 --- /dev/null +++ b/scripts/utils/environment.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Function to check if a command exists +command_exists() { + command -v "${1}" >/dev/null 2>&1 +} + +# Ensure required commands are available +ensure_required_commands() { + for cmd in docker; do + if ! command_exists "${cmd}"; then + error_exit "Required command '${cmd}' is not available. Please install it and try again." + fi + done +} + +# Ensure Docker daemon is running +ensure_docker_running() { + if ! docker info >/dev/null 2>&1; then + error_exit "Cannot connect to the Docker daemon. Is it running and do you have the right permissions?" + fi +} + +# Remove directory if it exists +remove_directory() { + local dir="${1}" + if [ -d "${dir}" ]; then + run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" + fi +} + +# Wait for a Docker container port to be available +wait_for_port() { + local container="${1}" + local port="${2}" + + run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." + until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." + sleep 1 + done + run_quietly_if_ci echo "Port ${port} is now open on ${container}." +} diff --git a/scripts/utils/errors.sh b/scripts/utils/errors.sh new file mode 100644 index 00000000..60d23a58 --- /dev/null +++ b/scripts/utils/errors.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Print error message to stderr +print_error() { + local message="${1}" + printf "Error: %s\n" "${message}" >&2 +} + +# Print error message and exit +error_exit() { + print_error "${1}" + exit 1 +} + +# Run command quietly in CI mode +run_quietly_if_ci() { + if [ "${SCRIPT_MODE}" = "ci" ]; then + "$@" >/dev/null 2>&1 + else + "$@" + fi +} diff --git a/scripts/utils/test_modes/ci.sh b/scripts/utils/test_modes/ci.sh new file mode 100644 index 00000000..69d05955 --- /dev/null +++ b/scripts/utils/test_modes/ci.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# Run CI mode +run_ci() { + # Print message (quiet in CI mode) + run_quietly_if_ci echo "Running tests in CI mode..." + + # Validate arguments + local test_scenario="${1}" + local efss_platform_1="${2}" + local efss_platform_2="${3}" + + if [[ -z "${test_scenario}" || -z "${efss_platform_1}" || -z "${efss_platform_2}" ]]; then + error_exit "Usage: run_ci " + fi + + # Cypress config file path + local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # Ensure the config file actually exists + if [[ ! -f "${cypress_config}" ]]; then + error_exit "Cypress config file not found at '${cypress_config}'." + fi + + # Adjust Cypress configurations for non-default browser platforms + if [[ "${BROWSER_PLATFORM}" != "electron" ]]; then + # Disable video and video compression + sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" + sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" + fi + + # Extract major version numbers for EFSS platforms + local p1_ver="${EFSS_PLATFORM_1_VERSION%%.*}" + local p2_ver="${EFSS_PLATFORM_2_VERSION%%.*}" + + # Construct spec file path from the arguments + local spec_path="cypress/e2e/${test_scenario}/${efss_platform_1}-${p1_ver}-to-${efss_platform_2}-${p2_ver}.cy.js" + + # Run Cypress tests in headless mode + if ! create_cypress_ci "${spec_path}"; then + error_exit "Failed to run Cypress tests with spec '${spec_path}'." + fi + + # Revert Cypress configuration changes if we modified them + if [[ "${BROWSER_PLATFORM}" != "electron" ]]; then + sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" + sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" + fi + + # Perform cleanup after CI tests + echo "Cleaning up test environment..." + if [[ -x "${ENV_ROOT}/scripts/clean.sh" ]]; then + "${ENV_ROOT}/scripts/clean.sh" "no" + else + print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." + fi +} diff --git a/scripts/utils/test_modes/dev.sh b/scripts/utils/test_modes/dev.sh new file mode 100644 index 00000000..947028d6 --- /dev/null +++ b/scripts/utils/test_modes/dev.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# Print OCM test setup instructions +print_ocm_test_setup_instructions() { + echo "" + echo "Development environment setup complete." + echo "Access the following URLs in your browser:" + echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" + echo " Embedded Firefox -> http://localhost:5800" + echo "Note:" + echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" + echo "" + echo "Log in to EFSS platforms using the following credentials:" +} + +# Run development mode +run_dev() { + local url_line_1="${1}" + local url_line_2="${2}" + + # Quiet log in CI mode + run_quietly_if_ci echo "Setting up development environment..." + + # Create containers for Firefox, VNC, and Cypress (dev mode) + create_firefox + create_vnc + create_cypress_dev + + # Display setup instructions + print_ocm_test_setup_instructions + + # Echo the two lines passed as arguments + echo " ${url_line_1}" + echo " ${url_line_2}" +} diff --git a/scripts/utils/validation.sh b/scripts/utils/validation.sh new file mode 100644 index 00000000..1125bb37 --- /dev/null +++ b/scripts/utils/validation.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Validate that required files and directories exist +validate_files() { + # Check if TLS certificate files exist + if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + fi + if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + fi + + # Check if Firefox certificate files exist + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then + error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" + fi + if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then + error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" + fi + + # Check if Cypress configuration exists + if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then + error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + fi +} + +# Parse command-line arguments +parse_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" + SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" + + export EFSS_PLATFORM_1_VERSION="${EFSS_PLATFORM_1_VERSION}" + export EFSS_PLATFORM_2_VERSION="${EFSS_PLATFORM_2_VERSION}" + export SCRIPT_MODE="${SCRIPT_MODE}" + export BROWSER_PLATFORM="${BROWSER_PLATFORM}" +} From 23e8465ced5398f0e2aa814d3be434e2ee501834 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 20 Jan 2025 13:48:42 +0330 Subject: [PATCH 114/184] add: missing function --- scripts/utils.sh | 1 + scripts/utils/setup.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 scripts/utils/setup.sh diff --git a/scripts/utils.sh b/scripts/utils.sh index 0504ea1d..da922f62 100755 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -30,6 +30,7 @@ source_module "errors.sh" source_module "environment.sh" source_module "docker.sh" source_module "validation.sh" +source_module "setup.sh" # Source container modules for module in "${MODULES_DIR}/container"/*.sh; do diff --git a/scripts/utils/setup.sh b/scripts/utils/setup.sh new file mode 100644 index 00000000..66443f5e --- /dev/null +++ b/scripts/utils/setup.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Extract platform variables from script path +extract_platform_variables() { + # Extract the parent folder name as TEST_SCENARIO + local test_scenario + test_scenario="$(basename "$(dirname "${SOURCE}")")" + + # Extract the filename without the .sh extension + local filename + filename="$(basename "${SOURCE}" .sh)" + + # Split the filename by '-' to get EFSS_PLATFORM_1 and EFSS_PLATFORM_2 + local platform1 platform2 + IFS='-' read -r platform1 platform2 <<< "${filename}" + + # Export the variables so the rest of the script can use them + export TEST_SCENARIO="${test_scenario}" + export EFSS_PLATFORM_1="${platform1}" + export EFSS_PLATFORM_2="${platform2}" +} + +# Main setup function that orchestrates the initialization +setup() { + # Get platform dependent variables + extract_platform_variables + + # Ensure required commands (including Docker) are available + ensure_required_commands + ensure_docker_running + + # Parse CLI arguments + parse_arguments "$@" + + # Validate required files/directories + validate_files + + # Prepare the environment + prepare_environment +} \ No newline at end of file From 986c770b6212d2a9b2caa51c0655991332e4352b Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 20 Jan 2025 14:21:00 +0330 Subject: [PATCH 115/184] add: modifications for login case test --- scripts/utils/constants.sh | 7 ++++++- scripts/utils/setup.sh | 18 +++++++++++++++--- scripts/utils/validation.sh | 23 +++++++++++++++++++++-- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/scripts/utils/constants.sh b/scripts/utils/constants.sh index 4b12a794..2e896d7a 100644 --- a/scripts/utils/constants.sh +++ b/scripts/utils/constants.sh @@ -24,6 +24,11 @@ MARIADB_TAG=11.4.4 VNC_REPO=theasp/novnc VNC_TAG=latest -# Export the constants +# Default script modes and platforms +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" + +# Export all constants export CYPRESS_REPO CYPRESS_TAG FIREFOX_REPO FIREFOX_TAG export MARIADB_REPO MARIADB_TAG VNC_REPO VNC_TAG +export DEFAULT_SCRIPT_MODE DEFAULT_BROWSER_PLATFORM diff --git a/scripts/utils/setup.sh b/scripts/utils/setup.sh index 66443f5e..c849cb10 100644 --- a/scripts/utils/setup.sh +++ b/scripts/utils/setup.sh @@ -10,7 +10,15 @@ extract_platform_variables() { local filename filename="$(basename "${SOURCE}" .sh)" - # Split the filename by '-' to get EFSS_PLATFORM_1 and EFSS_PLATFORM_2 + # For login scenario, we only have one platform + if [ "${test_scenario}" = "login" ]; then + export TEST_SCENARIO="${test_scenario}" + export EFSS_PLATFORM_1="${filename}" + export EFSS_PLATFORM_2="" + return + fi + + # For other scenarios, split the filename by '-' to get both platforms local platform1 platform2 IFS='-' read -r platform1 platform2 <<< "${filename}" @@ -29,8 +37,12 @@ setup() { ensure_required_commands ensure_docker_running - # Parse CLI arguments - parse_arguments "$@" + # Parse CLI arguments based on test scenario + if [ "${TEST_SCENARIO}" = "login" ]; then + parse_login_arguments "$@" + else + parse_share_arguments "$@" + fi # Validate required files/directories validate_files diff --git a/scripts/utils/validation.sh b/scripts/utils/validation.sh index 1125bb37..7525f247 100644 --- a/scripts/utils/validation.sh +++ b/scripts/utils/validation.sh @@ -1,5 +1,10 @@ #!/usr/bin/env bash +# Default values for script modes and browser platform +DEFAULT_SCRIPT_MODE="dev" +DEFAULT_BROWSER_PLATFORM="electron" +export DEFAULT_SCRIPT_MODE DEFAULT_BROWSER_PLATFORM + # Validate that required files and directories exist validate_files() { # Check if TLS certificate files exist @@ -24,8 +29,22 @@ validate_files() { fi } -# Parse command-line arguments -parse_arguments() { +# Parse command-line arguments for login scenario +parse_login_arguments() { + EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" + SCRIPT_MODE="${2:-$DEFAULT_SCRIPT_MODE}" + BROWSER_PLATFORM="${3:-$DEFAULT_BROWSER_PLATFORM}" + + export EFSS_PLATFORM_1_VERSION="${EFSS_PLATFORM_1_VERSION}" + export SCRIPT_MODE="${SCRIPT_MODE}" + export BROWSER_PLATFORM="${BROWSER_PLATFORM}" + + # Set EFSS_PLATFORM_2_VERSION to empty for login scenario + export EFSS_PLATFORM_2_VERSION="" +} + +# Parse command-line arguments for share scenarios +parse_share_arguments() { EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_1_VERSION}" EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_2_VERSION}" SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" From 70444f9fe45c2268a77203dfd435b456b3665300 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 20 Jan 2025 14:38:59 +0330 Subject: [PATCH 116/184] fix: ci mode test handlers and adjust cypress test files names --- .../{nextcloud.cy.js => nextcloud-v27.cy.js} | 0 .../cypress/e2e/login/nextcloud-v28.cy.js | 22 ++++++++++++ .../e2e/login/{ocis.cy.js => ocis-v5.cy.js} | 0 .../login/{ocmstub.cy.js => ocmstub-v1.cy.js} | 0 .../{owncloud.cy.js => owncloud-v10.cy.js} | 0 .../login/{seafile.cy.js => seafile-11.cy.js} | 0 scripts/utils/test_modes/ci.sh | 34 ++++++++++++------- 7 files changed, 43 insertions(+), 13 deletions(-) rename cypress/ocm-test-suite/cypress/e2e/login/{nextcloud.cy.js => nextcloud-v27.cy.js} (100%) create mode 100644 cypress/ocm-test-suite/cypress/e2e/login/nextcloud-v28.cy.js rename cypress/ocm-test-suite/cypress/e2e/login/{ocis.cy.js => ocis-v5.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/login/{ocmstub.cy.js => ocmstub-v1.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/login/{owncloud.cy.js => owncloud-v10.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/login/{seafile.cy.js => seafile-11.cy.js} (100%) diff --git a/cypress/ocm-test-suite/cypress/e2e/login/nextcloud.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/nextcloud-v27.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/login/nextcloud.cy.js rename to cypress/ocm-test-suite/cypress/e2e/login/nextcloud-v27.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/login/nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/nextcloud-v28.cy.js new file mode 100644 index 00000000..213485dc --- /dev/null +++ b/cypress/ocm-test-suite/cypress/e2e/login/nextcloud-v28.cy.js @@ -0,0 +1,22 @@ +/** + * @fileoverview + * Cypress test suite for testing the login functionality of Nextcloud. + * This suite contains tests to validate successful login functionality using valid credentials. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + +describe('Nextcloud Login Tests', () => { + /** + * Test Case: Validates successful login to Nextcloud. + * This test logs into Nextcloud using valid credentials and checks for a successful login state. + */ + it('should successfully log into Nextcloud with valid credentials', () => { + // Define the Nextcloud instance URL and credentials from environment variables or use default values + const nextcloudUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const username = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; + const password = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + + cy.loginNextcloud(nextcloudUrl, username, password); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/login/ocis.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/ocis-v5.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/login/ocis.cy.js rename to cypress/ocm-test-suite/cypress/e2e/login/ocis-v5.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/login/ocmstub.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/ocmstub-v1.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/login/ocmstub.cy.js rename to cypress/ocm-test-suite/cypress/e2e/login/ocmstub-v1.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/login/owncloud.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/owncloud-v10.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/login/owncloud.cy.js rename to cypress/ocm-test-suite/cypress/e2e/login/owncloud-v10.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/login/seafile.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/seafile-11.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/login/seafile.cy.js rename to cypress/ocm-test-suite/cypress/e2e/login/seafile-11.cy.js diff --git a/scripts/utils/test_modes/ci.sh b/scripts/utils/test_modes/ci.sh index 69d05955..17955bdc 100644 --- a/scripts/utils/test_modes/ci.sh +++ b/scripts/utils/test_modes/ci.sh @@ -5,13 +5,15 @@ run_ci() { # Print message (quiet in CI mode) run_quietly_if_ci echo "Running tests in CI mode..." - # Validate arguments - local test_scenario="${1}" - local efss_platform_1="${2}" - local efss_platform_2="${3}" - - if [[ -z "${test_scenario}" || -z "${efss_platform_1}" || -z "${efss_platform_2}" ]]; then - error_exit "Usage: run_ci " + # Validate arguments based on scenario + if [ "${TEST_SCENARIO}" = "login" ]; then + if [[ -z "${EFSS_PLATFORM_1}" ]]; then + error_exit "Usage for login: ci " + fi + else + if [[ -z "${EFSS_PLATFORM_1}" || -z "${EFSS_PLATFORM_2}" ]]; then + error_exit "Usage for share: ci " + fi fi # Cypress config file path @@ -29,12 +31,18 @@ run_ci() { sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" fi - # Extract major version numbers for EFSS platforms - local p1_ver="${EFSS_PLATFORM_1_VERSION%%.*}" - local p2_ver="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Construct spec file path from the arguments - local spec_path="cypress/e2e/${test_scenario}/${efss_platform_1}-${p1_ver}-to-${efss_platform_2}-${p2_ver}.cy.js" + # Construct spec file path based on scenario + local spec_path + if [ "${TEST_SCENARIO}" = "login" ]; then + # For login tests, we only need one platform version + local p1_ver="${EFSS_PLATFORM_1_VERSION%%.*}" + spec_path="cypress/e2e/${TEST_SCENARIO}/${EFSS_PLATFORM_1}-${p1_ver}.cy.js" + else + # For share tests, we need both platform versions + local p1_ver="${EFSS_PLATFORM_1_VERSION%%.*}" + local p2_ver="${EFSS_PLATFORM_2_VERSION%%.*}" + spec_path="cypress/e2e/${TEST_SCENARIO}/${EFSS_PLATFORM_1}-${p1_ver}-to-${EFSS_PLATFORM_2}-${p2_ver}.cy.js" + fi # Run Cypress tests in headless mode if ! create_cypress_ci "${spec_path}"; then From bac6f0f78085e7c174a066df646a24802adac5ab Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:00:15 +0330 Subject: [PATCH 117/184] add: additional global variables --- .../invite-link/nextcloud-nextcloud.sh | 10 +++++----- scripts/utils/constants.sh | 13 ++++++++----- scripts/utils/validation.sh | 13 ++++--------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh index e7f5cf53..0e21b13e 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh @@ -52,7 +52,7 @@ MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" # Paths to required directories TEMP_DIR="temp" TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" +TLS_CERT_DIR="docker/tls/certificates" # 3rd party containerS CYPRESS_REPO=cypress/included @@ -248,7 +248,7 @@ create_nextcloud() { -e DBHOST="marianextcloud${number}.docker" \ -e USER="${user}" \ -e PASS="${password}" \ - -v "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}:/certificates" \ + -v "${ENV_ROOT}/${TLS_CERT_DIR}:/certificates" \ -v "${ENV_ROOT}/${TLS_CA_DIR}:/certificate-authority" \ -v "${ENV_ROOT}/${TEMP_DIR}/${init_script}":"/init.sh" \ -v "${ENV_ROOT}/docker/scripts/entrypoint.sh":"/entrypoint.sh" \ @@ -284,7 +284,7 @@ create_reva() { run_docker_container --detach --network="${DOCKER_NETWORK}" \ --name="reva${platform}${number}.docker" \ -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}:/certificates" \ + -v "${ENV_ROOT}/${TLS_CERT_DIR}:/certificates" \ -v "${ENV_ROOT}/${TLS_CA_DIR}:/certificate-authority" \ -v "${ENV_ROOT}/${TEMP_DIR}/reva/configs:/configs/revad" \ -v "${ENV_ROOT}/${TEMP_DIR}/reva/run.sh":"/usr/bin/run.sh" \ @@ -338,8 +338,8 @@ parse_arguments() { # ----------------------------------------------------------------------------------- validate_files() { # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + if [ ! -d "${ENV_ROOT}/${TLS_CERT_DIR}" ]; then + error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERT_DIR}" fi if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" diff --git a/scripts/utils/constants.sh b/scripts/utils/constants.sh index 2e896d7a..aec85aa6 100644 --- a/scripts/utils/constants.sh +++ b/scripts/utils/constants.sh @@ -9,10 +9,10 @@ MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" export MARIADB_ROOT_PASSWORD # Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERTIFICATES_DIR="docker/tls/certificates" -export TEMP_DIR TLS_CA_DIR TLS_CERTIFICATES_DIR +TEMP_DIR="${ENV_ROOT}/temp" +TLS_CA_DIR="${ENV_ROOT}/docker/tls/certificate-authority" +TLS_CERT_DIR="${ENV_ROOT}/docker/tls/certificates" +export TEMP_DIR TLS_CA_DIR TLS_CERT_DIR # 3rd party containers CYPRESS_REPO=cypress/included @@ -21,6 +21,8 @@ FIREFOX_REPO=jlesage/firefox FIREFOX_TAG=v24.11.1 MARIADB_REPO=mariadb MARIADB_TAG=11.4.4 +MEMCACHED_REPO=memcached +MEMCACHED_TAG=1.6.18 VNC_REPO=theasp/novnc VNC_TAG=latest @@ -30,5 +32,6 @@ DEFAULT_BROWSER_PLATFORM="electron" # Export all constants export CYPRESS_REPO CYPRESS_TAG FIREFOX_REPO FIREFOX_TAG -export MARIADB_REPO MARIADB_TAG VNC_REPO VNC_TAG +export MARIADB_REPO MARIADB_TAG MEMCACHED_REPO MEMCACHED_TAG +export VNC_REPO VNC_TAG export DEFAULT_SCRIPT_MODE DEFAULT_BROWSER_PLATFORM diff --git a/scripts/utils/validation.sh b/scripts/utils/validation.sh index 7525f247..01dc04c0 100644 --- a/scripts/utils/validation.sh +++ b/scripts/utils/validation.sh @@ -1,18 +1,13 @@ #!/usr/bin/env bash -# Default values for script modes and browser platform -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" -export DEFAULT_SCRIPT_MODE DEFAULT_BROWSER_PLATFORM - # Validate that required files and directories exist validate_files() { # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERTIFICATES_DIR}" + if [ ! -d "${TLS_CERT_DIR}" ]; then + error_exit "TLS certificates directory not found: ${TLS_CERT_DIR}" fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" + if [ ! -d "${TLS_CA_DIR}" ]; then + error_exit "TLS certificate authority directory not found: ${TLS_CA_DIR}" fi # Check if Firefox certificate files exist From 282a8952c0f6457aea86d44c0310749104162c01 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:00:46 +0330 Subject: [PATCH 118/184] add: ocis, ocmstub and seafile create functions --- dev/ocm-test-suite/login/nextcloud.sh | 356 ++++++++++---------------- dev/ocm-test-suite/login/ocis.sh | 321 ++++++++++------------- dev/ocm-test-suite/login/ocmstub.sh | 317 ++++++++++------------- dev/ocm-test-suite/login/owncloud.sh | 352 ++++++++++--------------- dev/ocm-test-suite/login/seafile.sh | 354 ++++++++++--------------- scripts/utils/container/ocis.sh | 33 +++ scripts/utils/container/ocmstub.sh | 20 ++ scripts/utils/container/seafile.sh | 69 +++++ 8 files changed, 808 insertions(+), 1014 deletions(-) create mode 100644 scripts/utils/container/ocis.sh create mode 100644 scripts/utils/container/ocmstub.sh create mode 100644 scripts/utils/container/seafile.sh diff --git a/dev/ocm-test-suite/login/nextcloud.sh b/dev/ocm-test-suite/login/nextcloud.sh index 0af86675..8a4e9741 100755 --- a/dev/ocm-test-suite/login/nextcloud.sh +++ b/dev/ocm-test-suite/login/nextcloud.sh @@ -1,226 +1,148 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_VERSION=${1:-"v27.1.11"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${2:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${3:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to Nextcloud OCM share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Nextcloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./nextcloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./nextcloud-nextcloud.sh v28.0.14 v27.1.11 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v27.1.11" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-ocm-test-suite.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE # +# Arguments: +# All command line arguments are passed to parse_arguments. # -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -createEfss nextcloud 1 einstein relativity nextcloud.sh "${EFSS_PLATFORM_VERSION}" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/login/nextcloud.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cyp ress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # username # password # image # tag + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev "https://nextcloud1.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/login/ocis.sh b/dev/ocm-test-suite/login/ocis.sh index d6c53dcc..3608547e 100755 --- a/dev/ocm-test-suite/login/ocis.sh +++ b/dev/ocm-test-suite/login/ocis.sh @@ -1,195 +1,146 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# oCIS version: -# - 5.0.9 -EFSS_PLATFORM_VERSION=${1:-"5.0.9"} - - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${2:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${3:-"electron"} +# ----------------------------------------------------------------------------------- +# Script to Test OCIS login flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as OCIS, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./ocis.sh [EFSS_PLATFORM_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_VERSION : Version of the EFSS platform (default: "v5.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./ocis.sh v5.0.9 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v5.0.9" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT else - "$@" + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} -function createEfssOcis() { - local number="${1}" - - redirect_to_null_cmd echo "creating efss ocis ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="ocis${number}.docker" \ - -e OCIS_LOG_LEVEL=info \ - -e OCIS_LOG_COLOR=true \ - -e OCIS_LOG_PRETTY=true \ - -e PROXY_HTTP_ADDR=0.0.0.0:443 \ - -e OCIS_URL="https://ocis${number}.docker" \ - -e OCIS_INSECURE=true \ - -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" \ - -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" \ - -e PROXY_ENABLE_BASIC_AUTH=true \ - -e IDM_ADMIN_PASSWORD=admin \ - -e IDM_CREATE_DEMO_USERS=true \ - -v "${ENV_ROOT}/temp/certificates:/certificates" \ - -v "${ENV_ROOT}/temp/certificate-authority:/certificate-authority" \ - --entrypoint /bin/sh \ - "owncloud/ocis:5.0.9@sha256:96671605863b38b0b8021400fdb2d843586dfa31451a8c7766f15eabe85d8267" \ - -c "ocis init || true; ocis server" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/certificates" - -# copy init files. -cp -f "${ENV_ROOT}/docker/tls/certificates/ocis"* "${ENV_ROOT}/temp/certificates" -cp -fr "${ENV_ROOT}/docker/tls/certificate-authority" "${ENV_ROOT}/temp/certificate-authority" - -# fix permissions. -chmod -R 777 "${ENV_ROOT}/temp/certificates" -chmod -R 777 "${ENV_ROOT}/temp/certificate-authority" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -############ -### oCIS ### -############ - -# syntax: -# createEfssOcis number. +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE # +# Arguments: +# All command line arguments are passed to parse_arguments. # -# number: should be unique for each oCIS, for example: you cannot have two oCIS with same number. - -# oCISes. -createEfssOcis 1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://ocis1.docker -> username: einstein password: relativity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/login/ocis.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # image # tag + create_ocis 1 owncloud/ocis "${EFSS_PLATFORM_1_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev "https://ocis1.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/login/ocmstub.sh b/dev/ocm-test-suite/login/ocmstub.sh index 0c576d45..e1a22886 100755 --- a/dev/ocm-test-suite/login/ocmstub.sh +++ b/dev/ocm-test-suite/login/ocmstub.sh @@ -1,189 +1,146 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# ocmstub version: -# - 1.0 -EFSS_PLATFORM_VERSION=${1:-"1.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${2:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${3:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" +# ----------------------------------------------------------------------------------- +# Script to Test OCMStub login flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as OCMStub, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./ocmstub.sh [EFSS_PLATFORM_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_VERSION : Version of the EFSS platform (default: "v1.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./ocmstub.sh v1.0.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v1.0.0" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } -function createEfss() { - local platform="${1}" - local number="${2}" - local tag="${3-latest}" - local image="${4}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/tls/${platform}${number}.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/tls/${platform}${number}.key" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "${platform}${number}.docker" 443 +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi - redirect_to_null_cmd echo "" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################# -### ocmstub ### -################# - -# syntax: -# createEfss platform number username password image. +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE # +# Arguments: +# All command line arguments are passed to parse_arguments. # -# platform: ocmstub. -# number: should be unique for each platform, for example: you cannot have two ocmstubs with same number. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ocmstub only has the latest tag so we don't need this "${EFSS_PLATFORM_VERSION}" -createEfss ocmstub 1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://ocmstub1.docker/? -> click 'Log in'" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/login/ocmstub.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cyp ress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # image # tag + create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_1_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev "https://ocmstub1.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/login/owncloud.sh b/dev/ocm-test-suite/login/owncloud.sh index dd050ac2..70755667 100755 --- a/dev/ocm-test-suite/login/owncloud.sh +++ b/dev/ocm-test-suite/login/owncloud.sh @@ -1,224 +1,146 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_VERSION=${1:-"v10.15.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${2:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${3:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi +# ----------------------------------------------------------------------------------- +# Script to Test OwnCloud login flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as OwnCloud, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./owncloud.sh [EFSS_PLATFORM_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_VERSION : Version of the EFSS platform (default: "v10.13.1"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./owncloud.sh v10.13.1 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v10.13.1" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sm-ocm.sh" "${ENV_ROOT}/temp/owncloud.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################ -### ownCloud ### -################ - -# syntax: -# createEfss platform number username password image. +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE # +# Arguments: +# All command line arguments are passed to parse_arguments. # -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -createEfss owncloud 1 marie radioactivity owncloud.sh latest ocm-test-suite - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/login/owncloud.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # username # password # image # tag + create_owncloud 1 "einstein" "relativity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev "https://owncloud1.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/login/seafile.sh b/dev/ocm-test-suite/login/seafile.sh index a53b3416..fd7253e5 100755 --- a/dev/ocm-test-suite/login/seafile.sh +++ b/dev/ocm-test-suite/login/seafile.sh @@ -1,226 +1,146 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# seafile version: -# - 8.0.8 -# - 9.0.10 -# - 10.0.1 -# - 11.0.5 -EFSS_PLATFORM_VERSION=${1:-"11.0.5"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${2:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${3:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +# ----------------------------------------------------------------------------------- +# Script to Test Seafile login flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as Seafile, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./seafile.sh [EFSS_PLATFORM_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_VERSION : Version of the EFSS platform (default: "v11.0.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Requirements: +# - Docker and required images must be installed. +# - Test scripts and configurations must be located in the expected directories. +# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. + +# Example: +# ./seafile.sh v11.0.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v11.0.0" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT else - "$@" + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi -} -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -function createEfssSeafile() { - local platform="${1}" - local number="${2}" - local user_email="${3}" - local password="${4}" - local remote_ocm_server="${5}" - local tag="${6-latest}" - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="memcache${platform}${number}.docker" \ - memcached:1.6.18 \ - memcached -m 256 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - -e TIME_ZONE="Etc/UTC" \ - -e DB_HOST="maria${platform}${number}.docker" \ - -e DB_ROOT_PASSWD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" \ - -e SEAFILE_ADMIN_EMAIL="${user_email}" \ - -e SEAFILE_ADMIN_PASSWORD="${password}" \ - -e SEAFILE_SERVER_LETSENCRYPT=false \ - -e FORCE_HTTPS_IN_CONF=false \ - -e SEAFILE_SERVER_HOSTNAME="${platform}${number}.docker" \ - -e SEAFILE_MEMCACHE_HOST="memcache${platform}${number}.docker" \ - -e SEAFILE_MEMCACHE_PORT=11211 \ - -v "${ENV_ROOT}/temp/sea-init.sh:/init.sh" \ - -v "${ENV_ROOT}/temp/seafile-data/${platform}${number}:/shared" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/shared/ssl/${platform}${number}.docker.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/shared/ssl/${platform}${number}.docker.key" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - "seafileltd/seafile-mc:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - - # seafile needs time to bootstrap itself. - sleep 5 - - # run init script inside seafile. - redirect_to_null_cmd docker exec -e remote_ocm_server="${remote_ocm_server}" "${platform}${number}.docker" bash -c "/init.sh ${remote_ocm_server}" - - # restart seafile to apply our changes. - sleep 2 - redirect_to_null_cmd docker restart "${platform}${number}.docker" - sleep 2 - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # username # password # image # tag # remote_ocm_server + create_seafile 1 "einstein" "relativity" seafileltd/seafile-mc "${EFSS_PLATFORM_1_VERSION}" seafile1 + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev "https://seafile1.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/seafile.sh" "${ENV_ROOT}/temp/sea-init.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -############### -### Seafile ### -############### - -createEfssSeafile seafile 1 jonathan@seafile.com xu seafile2 "${EFSS_PLATFORM_VERSION}" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "http://seafile1.docker -> username: jonathan@seafile.com password: xu" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/login/seafile.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh new file mode 100644 index 00000000..15eb7cc1 --- /dev/null +++ b/scripts/utils/container/ocis.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Create an OCIS container +create_ocis() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocis ${number}" + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="ocis${number}.docker" \ + -e OCIS_LOG_LEVEL=info \ + -e OCIS_LOG_COLOR=true \ + -e OCIS_LOG_PRETTY=true \ + -e PROXY_HTTP_ADDR=0.0.0.0:443 \ + -e OCIS_URL="https://ocis${number}.docker" \ + -e OCIS_INSECURE=true\ + -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" + -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" + -e PROXY_ENABLE_BASIC_AUTH=true \ + -e IDM_ADMIN_PASSWORD=admin \ + -e IDM_CREATE_DEMO_USERS=true \ + -v "${ENV_ROOT}/temp/certificates:/certificates" \ + -v "${ENV_ROOT}/temp/certificate-authority:/certificate-authority" \ + --entrypoint /bin/sh \ + "${image}:${tag#v}" \ + -c "ocis init || true; ocis server" || error_exit "Failed to start EFSS container for ocis ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocis${number}.docker" 9200 +} diff --git a/scripts/utils/container/ocmstub.sh b/scripts/utils/container/ocmstub.sh new file mode 100644 index 00000000..a27bea85 --- /dev/null +++ b/scripts/utils/container/ocmstub.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Create an OCMStub container +create_ocmstub() { + local number="${1}" + local image="${2}" + local tag="${3}" + + run_quietly_if_ci echo "Creating EFSS instance: ocmstub ${number}" + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}.docker" \ + -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ + "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "ocmstub${number}.docker" 443 +} diff --git a/scripts/utils/container/seafile.sh b/scripts/utils/container/seafile.sh new file mode 100644 index 00000000..c9cb2305 --- /dev/null +++ b/scripts/utils/container/seafile.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Create a Seafile container with MariaDB and Memcached backends +create_seafile() { + local number="${1}" + local user="${2}" + local password="${3}" + local image="${4}" + local tag="${5}" + local remote_ocm_server="${6}" + + run_quietly_if_ci echo "Creating EFSS instance: seafile ${number}" + + # Start Memcached container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="memcacheseafile${number}.docker" \ + "${MEMCACHED_REPO}:${MEMCACHED_TAG}" \ + memcached -m 256 || error_exit "Failed to start Memcached container for seafile ${number}." + + # Start MariaDB container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="mariaseafile${number}.docker" \ + -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ + "${MARIADB_REPO}:${MARIADB_TAG}" \ + --transaction-isolation=READ-COMMITTED \ + --log-bin=binlog \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for seafile ${number}." + + # Wait for MariaDB port to open + wait_for_port "mariaseafile${number}.docker" 3306 + + # Start EFSS container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="seafile${number}.docker" \ + -e TIME_ZONE="Etc/UTC" \ + -e DB_HOST="mariaseafile${number}.docker" \ + -e DB_ROOT_PASSWD="${MARIADB_ROOT_PASSWORD}" \ + -e SEAFILE_ADMIN_EMAIL="${user}" \ + -e SEAFILE_ADMIN_PASSWORD="${password}" \ + -e SEAFILE_SERVER_LETSENCRYPT=false \ + -e FORCE_HTTPS_IN_CONF=false \ + -e SEAFILE_SERVER_HOSTNAME="seafile${number}.docker" \ + -e SEAFILE_MEMCACHE_HOST="memcacheseafile${number}.docker" \ + -e SEAFILE_MEMCACHE_PORT=11211 \ + -v "${TLS_CA_DIR}:/certificate-authority" \ + -v "${TLS_CERT_DIR}:/certificates" \ + -v "${TLS_CERT_DIR}/seafile${number}.crt:/shared/ssl/seafile${number}.docker.crt" \ + -v "${TLS_CERT_DIR}/seafile${number}.key:/shared/ssl/seafile${number}.docker.key" \ + "${image}:${tag#v}" || error_exit "Failed to start EFSS container for seafile ${number}." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "seafile${number}.docker" 443 + + # seafile needs time to bootstrap itself. + sleep 5 + + # run init script inside seafile. + run_quietly_if_ci docker exec -e remote_ocm_server="${remote_ocm_server}" "seafile${number}.docker" bash -c "/init.sh ${remote_ocm_server}" + + # restart seafile to apply our changes. + sleep 2 + run_quietly_if_ci docker restart "seafile${number}.docker" + sleep 2 + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "seafile${number}.docker" 443 +} From 645b5d84090832ebdcd67a3f454effe6ec4610ac Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:05:14 +0330 Subject: [PATCH 119/184] fix: unbound variable in login tests --- dev/ocm-test-suite/login/nextcloud.sh | 3 ++- dev/ocm-test-suite/login/ocis.sh | 2 ++ dev/ocm-test-suite/login/ocmstub.sh | 2 ++ dev/ocm-test-suite/login/owncloud.sh | 2 ++ dev/ocm-test-suite/login/seafile.sh | 2 ++ 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/dev/ocm-test-suite/login/nextcloud.sh b/dev/ocm-test-suite/login/nextcloud.sh index 8a4e9741..fd20f738 100755 --- a/dev/ocm-test-suite/login/nextcloud.sh +++ b/dev/ocm-test-suite/login/nextcloud.sh @@ -40,7 +40,8 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v27.1.11" -DEFAULT_EFSS_2_VERSION="v27.1.11" +# For login tests, we don't need a second platform version +DEFAULT_EFSS_2_VERSION="" # ----------------------------------------------------------------------------------- # Function: resolve_script_dir diff --git a/dev/ocm-test-suite/login/ocis.sh b/dev/ocm-test-suite/login/ocis.sh index 3608547e..3862235b 100755 --- a/dev/ocm-test-suite/login/ocis.sh +++ b/dev/ocm-test-suite/login/ocis.sh @@ -39,6 +39,8 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v5.0.9" +# For login tests, we don't need a second platform version +DEFAULT_EFSS_2_VERSION="" # ----------------------------------------------------------------------------------- # Function: resolve_script_dir diff --git a/dev/ocm-test-suite/login/ocmstub.sh b/dev/ocm-test-suite/login/ocmstub.sh index e1a22886..cf85cbfd 100755 --- a/dev/ocm-test-suite/login/ocmstub.sh +++ b/dev/ocm-test-suite/login/ocmstub.sh @@ -39,6 +39,8 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v1.0.0" +# For login tests, we don't need a second platform version +DEFAULT_EFSS_2_VERSION="" # ----------------------------------------------------------------------------------- # Function: resolve_script_dir diff --git a/dev/ocm-test-suite/login/owncloud.sh b/dev/ocm-test-suite/login/owncloud.sh index 70755667..489b437f 100755 --- a/dev/ocm-test-suite/login/owncloud.sh +++ b/dev/ocm-test-suite/login/owncloud.sh @@ -39,6 +39,8 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v10.13.1" +# For login tests, we don't need a second platform version +DEFAULT_EFSS_2_VERSION="" # ----------------------------------------------------------------------------------- # Function: resolve_script_dir diff --git a/dev/ocm-test-suite/login/seafile.sh b/dev/ocm-test-suite/login/seafile.sh index fd7253e5..3c4868d4 100755 --- a/dev/ocm-test-suite/login/seafile.sh +++ b/dev/ocm-test-suite/login/seafile.sh @@ -39,6 +39,8 @@ set -euo pipefail # Default versions DEFAULT_EFSS_1_VERSION="v11.0.0" +# For login tests, we don't need a second platform version +DEFAULT_EFSS_2_VERSION="" # ----------------------------------------------------------------------------------- # Function: resolve_script_dir From d5bd04a016adcd7d7cc5ce60a7cf29459386d799 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:06:53 +0330 Subject: [PATCH 120/184] fix: missing backslash in commands --- scripts/utils/container/ocis.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh index 15eb7cc1..e705c4ea 100644 --- a/scripts/utils/container/ocis.sh +++ b/scripts/utils/container/ocis.sh @@ -16,10 +16,10 @@ create_ocis() { -e OCIS_LOG_PRETTY=true \ -e PROXY_HTTP_ADDR=0.0.0.0:443 \ -e OCIS_URL="https://ocis${number}.docker" \ - -e OCIS_INSECURE=true\ - -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" - -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" - -e PROXY_ENABLE_BASIC_AUTH=true \ + -e OCIS_INSECURE=true \ + -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" \ + -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" \ + -e PROXY_ENABLE_BASIC_AUTH=true \ -e IDM_ADMIN_PASSWORD=admin \ -e IDM_CREATE_DEMO_USERS=true \ -v "${ENV_ROOT}/temp/certificates:/certificates" \ From 0fc640feb0375285901625b533a781d19b0d044c Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:10:14 +0330 Subject: [PATCH 121/184] fix: update port number for EFSS container in ocis.sh --- scripts/utils/container/ocis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh index e705c4ea..c026c946 100644 --- a/scripts/utils/container/ocis.sh +++ b/scripts/utils/container/ocis.sh @@ -29,5 +29,5 @@ create_ocis() { -c "ocis init || true; ocis server" || error_exit "Failed to start EFSS container for ocis ${number}." # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocis${number}.docker" 9200 + run_quietly_if_ci wait_for_port "ocis${number}.docker" 443 } From bc38af76ffd9010d80548fd29a3de5a1fc97faec Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:14:48 +0330 Subject: [PATCH 122/184] fix: update run_dev command in login scripts to include empty argument for password --- dev/ocm-test-suite/login/nextcloud.sh | 2 +- dev/ocm-test-suite/login/ocis.sh | 2 +- dev/ocm-test-suite/login/ocmstub.sh | 2 +- dev/ocm-test-suite/login/owncloud.sh | 2 +- dev/ocm-test-suite/login/seafile.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/ocm-test-suite/login/nextcloud.sh b/dev/ocm-test-suite/login/nextcloud.sh index fd20f738..725e6e80 100755 --- a/dev/ocm-test-suite/login/nextcloud.sh +++ b/dev/ocm-test-suite/login/nextcloud.sh @@ -137,7 +137,7 @@ main() { create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - run_dev "https://nextcloud1.docker (username: einstein, password: relativity)" + run_dev "https://nextcloud1.docker (username: einstein, password: relativity)" "" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" fi diff --git a/dev/ocm-test-suite/login/ocis.sh b/dev/ocm-test-suite/login/ocis.sh index 3862235b..1efd7464 100755 --- a/dev/ocm-test-suite/login/ocis.sh +++ b/dev/ocm-test-suite/login/ocis.sh @@ -136,7 +136,7 @@ main() { create_ocis 1 owncloud/ocis "${EFSS_PLATFORM_1_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - run_dev "https://ocis1.docker (username: einstein, password: relativity)" + run_dev "https://ocis1.docker (username: einstein, password: relativity)" "" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" fi diff --git a/dev/ocm-test-suite/login/ocmstub.sh b/dev/ocm-test-suite/login/ocmstub.sh index cf85cbfd..9bd7670e 100755 --- a/dev/ocm-test-suite/login/ocmstub.sh +++ b/dev/ocm-test-suite/login/ocmstub.sh @@ -136,7 +136,7 @@ main() { create_ocmstub 1 pondersource/ocmstub "${EFSS_PLATFORM_1_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - run_dev "https://ocmstub1.docker (username: einstein, password: relativity)" + run_dev "https://ocmstub1.docker (username: einstein, password: relativity)" "" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" fi diff --git a/dev/ocm-test-suite/login/owncloud.sh b/dev/ocm-test-suite/login/owncloud.sh index 489b437f..dcd4cd9e 100755 --- a/dev/ocm-test-suite/login/owncloud.sh +++ b/dev/ocm-test-suite/login/owncloud.sh @@ -136,7 +136,7 @@ main() { create_owncloud 1 "einstein" "relativity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then - run_dev "https://owncloud1.docker (username: einstein, password: relativity)" + run_dev "https://owncloud1.docker (username: einstein, password: relativity)" "" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" fi diff --git a/dev/ocm-test-suite/login/seafile.sh b/dev/ocm-test-suite/login/seafile.sh index 3c4868d4..1edfd5b9 100755 --- a/dev/ocm-test-suite/login/seafile.sh +++ b/dev/ocm-test-suite/login/seafile.sh @@ -136,7 +136,7 @@ main() { create_seafile 1 "einstein" "relativity" seafileltd/seafile-mc "${EFSS_PLATFORM_1_VERSION}" seafile1 if [ "${SCRIPT_MODE}" = "dev" ]; then - run_dev "https://seafile1.docker (username: einstein, password: relativity)" + run_dev "https://seafile1.docker (username: einstein, password: relativity)" "" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" fi From 83f9eb96ac19e60826c5a63a1963f2c6f7455185 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:19:04 +0330 Subject: [PATCH 123/184] fix: update volume mounts in ocis.sh to use TLS_CERT_DIR and TLS_CA_DIR variables --- scripts/utils/container/ocis.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh index c026c946..03089f2b 100644 --- a/scripts/utils/container/ocis.sh +++ b/scripts/utils/container/ocis.sh @@ -22,8 +22,8 @@ create_ocis() { -e PROXY_ENABLE_BASIC_AUTH=true \ -e IDM_ADMIN_PASSWORD=admin \ -e IDM_CREATE_DEMO_USERS=true \ - -v "${ENV_ROOT}/temp/certificates:/certificates" \ - -v "${ENV_ROOT}/temp/certificate-authority:/certificate-authority" \ + -v "${TLS_CERT_DIR}:/certificates" \ + -v "${TLS_CA_DIR}:/certificate-authority" \ --entrypoint /bin/sh \ "${image}:${tag#v}" \ -c "ocis init || true; ocis server" || error_exit "Failed to start EFSS container for ocis ${number}." From 54513fb8fe9d4efe69a5be99f60504f45a5764e3 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:26:34 +0330 Subject: [PATCH 124/184] refactor: comment out wait_for_port in ocis.sh and add TODO for custom images --- scripts/utils/container/ocis.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh index 03089f2b..4a948e73 100644 --- a/scripts/utils/container/ocis.sh +++ b/scripts/utils/container/ocis.sh @@ -29,5 +29,6 @@ create_ocis() { -c "ocis init || true; ocis server" || error_exit "Failed to start EFSS container for ocis ${number}." # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "ocis${number}.docker" 443 + # TODO @MahdiBaghbani: we might need custom images with ss installed. + # run_quietly_if_ci wait_for_port "ocis${number}.docker" 443 } From 2850c9b25deb5812bdf50b4486dcdfbd0a1d0fcd Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:28:25 +0330 Subject: [PATCH 125/184] fix: update volume mounts in ocmstub.sh to use TLS_CERT_DIR and TLS_CA_DIR variables --- scripts/utils/container/ocmstub.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/utils/container/ocmstub.sh b/scripts/utils/container/ocmstub.sh index a27bea85..da3ca391 100644 --- a/scripts/utils/container/ocmstub.sh +++ b/scripts/utils/container/ocmstub.sh @@ -12,7 +12,8 @@ create_ocmstub() { run_docker_container --detach --network="${DOCKER_NETWORK}" \ --name="ocmstub${number}.docker" \ -e HOST="ocmstub${number}.docker" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ + -v "${TLS_CERT_DIR}:/tls" \ + -v "${TLS_CA_DIR}:/tls-ca" \ "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." # Wait for EFSS port to open From 573d0b8b52573115067fe77afa2cadca281f36dd Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:29:49 +0330 Subject: [PATCH 126/184] fix: cert volume mount path --- scripts/utils/container/ocmstub.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/utils/container/ocmstub.sh b/scripts/utils/container/ocmstub.sh index da3ca391..be0faa06 100644 --- a/scripts/utils/container/ocmstub.sh +++ b/scripts/utils/container/ocmstub.sh @@ -12,8 +12,8 @@ create_ocmstub() { run_docker_container --detach --network="${DOCKER_NETWORK}" \ --name="ocmstub${number}.docker" \ -e HOST="ocmstub${number}.docker" \ - -v "${TLS_CERT_DIR}:/tls" \ - -v "${TLS_CA_DIR}:/tls-ca" \ + -v "${TLS_CERT_DIR}:/certificates" \ + -v "${TLS_CA_DIR}:/certificate-authority" \ "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." # Wait for EFSS port to open From 355efe8d4771d5dd4873f2e43100434b1f016f4c Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 10:37:12 +0330 Subject: [PATCH 127/184] fix: correct HOST environment variable in ocmstub.sh to remove '.docker' suffix --- scripts/utils/container/ocmstub.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/container/ocmstub.sh b/scripts/utils/container/ocmstub.sh index be0faa06..a9d24f19 100644 --- a/scripts/utils/container/ocmstub.sh +++ b/scripts/utils/container/ocmstub.sh @@ -11,7 +11,7 @@ create_ocmstub() { # Start EFSS container run_docker_container --detach --network="${DOCKER_NETWORK}" \ --name="ocmstub${number}.docker" \ - -e HOST="ocmstub${number}.docker" \ + -e HOST="ocmstub${number}" \ -v "${TLS_CERT_DIR}:/certificates" \ -v "${TLS_CA_DIR}:/certificate-authority" \ "${image}:${tag}" || error_exit "Failed to start EFSS container for ocmstub ${number}." From b294b2f6ae3404d4417d7207742fd8793327cbb4 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 11:08:35 +0330 Subject: [PATCH 128/184] refactor: comment out wait_for_port in seafile.sh and add TODO for potential custom images --- scripts/utils/container/seafile.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/utils/container/seafile.sh b/scripts/utils/container/seafile.sh index c9cb2305..df5a568f 100644 --- a/scripts/utils/container/seafile.sh +++ b/scripts/utils/container/seafile.sh @@ -51,7 +51,8 @@ create_seafile() { "${image}:${tag#v}" || error_exit "Failed to start EFSS container for seafile ${number}." # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "seafile${number}.docker" 443 + # TODO @MahdiBaghbani: we might need custom images with ss installed. + # run_quietly_if_ci wait_for_port "seafile${number}.docker" 443 # seafile needs time to bootstrap itself. sleep 5 @@ -65,5 +66,6 @@ create_seafile() { sleep 2 # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "seafile${number}.docker" 443 + # TODO @MahdiBaghbani: we might need custom images with ss installed. + # run_quietly_if_ci wait_for_port "seafile${number}.docker" 443 } From b2be514c7ead1e150d37653b2c7e36671684d50d Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 12:27:32 +0330 Subject: [PATCH 129/184] feat: add Seafile Seahub settings configuration script - Introduced a new script `seafile.sh` for configuring Seafile Seahub settings. - The script enables Open Cloud Mesh (OCM) integration and updates Memcached settings based on environment variables or defaults. - Added error handling for file permissions and missing files. - Updated `constants.sh` to export the new `DOCKER_SCRIPTS_DIR` variable. - Modified `seafile.sh` container setup to mount the new script as an initialization script. --- docker/scripts/{init => seafile}/seafile.sh | 0 scripts/utils/constants.sh | 3 ++- scripts/utils/container/seafile.sh | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) rename docker/scripts/{init => seafile}/seafile.sh (100%) mode change 100755 => 100644 diff --git a/docker/scripts/init/seafile.sh b/docker/scripts/seafile/seafile.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/seafile.sh rename to docker/scripts/seafile/seafile.sh diff --git a/scripts/utils/constants.sh b/scripts/utils/constants.sh index aec85aa6..2284b691 100644 --- a/scripts/utils/constants.sh +++ b/scripts/utils/constants.sh @@ -12,7 +12,8 @@ export MARIADB_ROOT_PASSWORD TEMP_DIR="${ENV_ROOT}/temp" TLS_CA_DIR="${ENV_ROOT}/docker/tls/certificate-authority" TLS_CERT_DIR="${ENV_ROOT}/docker/tls/certificates" -export TEMP_DIR TLS_CA_DIR TLS_CERT_DIR +DOCKER_SCRIPTS_DIR="${ENV_ROOT}/docker/scripts" +export TEMP_DIR TLS_CA_DIR TLS_CERT_DIR DOCKER_SCRIPTS_DIR # 3rd party containers CYPRESS_REPO=cypress/included diff --git a/scripts/utils/container/seafile.sh b/scripts/utils/container/seafile.sh index df5a568f..6daac061 100644 --- a/scripts/utils/container/seafile.sh +++ b/scripts/utils/container/seafile.sh @@ -48,6 +48,7 @@ create_seafile() { -v "${TLS_CERT_DIR}:/certificates" \ -v "${TLS_CERT_DIR}/seafile${number}.crt:/shared/ssl/seafile${number}.docker.crt" \ -v "${TLS_CERT_DIR}/seafile${number}.key:/shared/ssl/seafile${number}.docker.key" \ + -v "${DOCKER_SCRIPTS_DIR}/seafile/seafile.sh:/init.sh" \ "${image}:${tag#v}" || error_exit "Failed to start EFSS container for seafile ${number}." # Wait for EFSS port to open From d3f274d26dc433eef76b0bdfeecebea2bebcea3b Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 12:29:40 +0330 Subject: [PATCH 130/184] fix: ensure init script is executable in seafile.sh --- scripts/utils/container/seafile.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/container/seafile.sh b/scripts/utils/container/seafile.sh index 6daac061..22e58f78 100644 --- a/scripts/utils/container/seafile.sh +++ b/scripts/utils/container/seafile.sh @@ -59,7 +59,7 @@ create_seafile() { sleep 5 # run init script inside seafile. - run_quietly_if_ci docker exec -e remote_ocm_server="${remote_ocm_server}" "seafile${number}.docker" bash -c "/init.sh ${remote_ocm_server}" + run_quietly_if_ci docker exec -e remote_ocm_server="${remote_ocm_server}" "seafile${number}.docker" bash -c "chmod +x /init.sh && /init.sh ${remote_ocm_server}" # restart seafile to apply our changes. sleep 2 From cbebfafe70e72036fd8a370a70dba603424ad6ff Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 12:49:52 +0330 Subject: [PATCH 131/184] fix: cypress test filename and credentials --- .../e2e/login/{seafile-11.cy.js => seafile-v11.cy.js} | 0 dev/ocm-test-suite/login/seafile.sh | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename cypress/ocm-test-suite/cypress/e2e/login/{seafile-11.cy.js => seafile-v11.cy.js} (100%) diff --git a/cypress/ocm-test-suite/cypress/e2e/login/seafile-11.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/seafile-v11.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/login/seafile-11.cy.js rename to cypress/ocm-test-suite/cypress/e2e/login/seafile-v11.cy.js diff --git a/dev/ocm-test-suite/login/seafile.sh b/dev/ocm-test-suite/login/seafile.sh index 1edfd5b9..153a105f 100755 --- a/dev/ocm-test-suite/login/seafile.sh +++ b/dev/ocm-test-suite/login/seafile.sh @@ -132,11 +132,11 @@ main() { setup "$@" # Create EFSS containers - # # id # username # password # image # tag # remote_ocm_server - create_seafile 1 "einstein" "relativity" seafileltd/seafile-mc "${EFSS_PLATFORM_1_VERSION}" seafile1 + # # id # username # password # image # tag # remote_ocm_server + create_seafile 1 "jonathan@seafile.com" "xu" seafileltd/seafile-mc "${EFSS_PLATFORM_1_VERSION}" seafile1 if [ "${SCRIPT_MODE}" = "dev" ]; then - run_dev "https://seafile1.docker (username: einstein, password: relativity)" "" + run_dev "https://seafile1.docker (username: jonathan@seafile.com, password: xu)" "" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" fi From 33547a1ccb08d806555ab63761239d2877744d08 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 12:50:39 +0330 Subject: [PATCH 132/184] refactor: simplify loginSeafile command in Cypress tests --- cypress/ocm-test-suite/cypress/support/commands.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/support/commands.js b/cypress/ocm-test-suite/cypress/support/commands.js index 2f0efb2d..af96367b 100644 --- a/cypress/ocm-test-suite/cypress/support/commands.js +++ b/cypress/ocm-test-suite/cypress/support/commands.js @@ -58,15 +58,10 @@ Cypress.Commands.add('loginNextcloud', (url, username, password) => { Cypress.Commands.add('loginSeafile', (url, username, password) => { cy.visit(url); - // Ensure the login page is visible - cy.get('#wrapper #log-in-panel #login-form', { timeout: 10000 }).should('be.visible'); - // Fill in login credentials and submit - cy.get('#wrapper #log-in-panel #login-form').within(() => { - cy.get('input[name="login"]').type(username); - cy.get('input[name="password"]').type(password); - cy.get('button[type="submit"]').click(); - }); + cy.get('input[name="login"]').type(username); + cy.get('input[name="password"]').type(password); + cy.get('button[type="submit"]').click(); }); /** From b3c7c4df142cb6eeb8392025eadaf98b2567246f Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 12:51:24 +0330 Subject: [PATCH 133/184] fix: update login function and URL protocol in Seafile Cypress tests --- cypress/ocm-test-suite/cypress/e2e/login/seafile-v11.cy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/login/seafile-v11.cy.js b/cypress/ocm-test-suite/cypress/e2e/login/seafile-v11.cy.js index 7d96fc1d..c30373b8 100644 --- a/cypress/ocm-test-suite/cypress/e2e/login/seafile-v11.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/login/seafile-v11.cy.js @@ -13,10 +13,10 @@ describe('Seafile Login Tests', () => { */ it('should successfully log into Seafile with valid credentials', () => { // Define the Seafile instance URL and credentials from environment variables or use default values - const seafileUrl = Cypress.env('SEAFILE1_URL') || 'https://seafile1.docker'; + const seafileUrl = Cypress.env('SEAFILE1_URL') || 'http://seafile1.docker'; const username = Cypress.env('SEAFILE1_USERNAME') || 'jonathan@seafile.com'; const password = Cypress.env('SEAFILE1_PASSWORD') || 'xu'; - cy.loginOcis(seafileUrl, username, password); + cy.loginSeafile(seafileUrl, username, password); }); }); From 611b61506cd842da89c6d18671bab551f77eda90 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 22 Jan 2025 09:47:02 +0000 Subject: [PATCH 134/184] modify: I have no idea what has changed in this file --- docker/scripts/seafile/seafile.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docker/scripts/seafile/seafile.sh diff --git a/docker/scripts/seafile/seafile.sh b/docker/scripts/seafile/seafile.sh old mode 100644 new mode 100755 From 59ecb57ea4c4a3a645c3b478ed0e6f83e9d97de8 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 13:54:15 +0330 Subject: [PATCH 135/184] update: version formatting in workflow files for consistency --- .github/workflows/invite-link-nc-v27-ocis-v5.yml | 2 +- .github/workflows/invite-link-oc-v10-ocis-v5.yml | 2 +- .github/workflows/invite-link-ocis-v5-nc-v27.yml | 2 +- .github/workflows/invite-link-ocis-v5-oc-v10.yml | 2 +- .github/workflows/invite-link-ocis-v5-ocis-v5.yml | 4 ++-- .github/workflows/login-ocis-v5.yml | 2 +- .github/workflows/login-ocmstub-v1.yml | 2 +- .github/workflows/login-seafile-v11.yml | 2 +- .github/workflows/share-link-nc-v27-os-v1.yml | 2 +- .github/workflows/share-with-nc-v27-os-v1.yml | 2 +- .github/workflows/share-with-nc-v28-os-v1.yml | 2 +- .github/workflows/share-with-oc-v10-os-v1.yml | 2 +- .github/workflows/share-with-os-v1-nc-v27.yml | 2 +- .github/workflows/share-with-os-v1-nc-v28.yml | 2 +- .github/workflows/share-with-os-v1-oc-10.yml | 2 +- .github/workflows/share-with-os-v1-os-v1.yml | 4 ++-- .github/workflows/share-with-sf-v11-sf-v11.yml | 4 ++-- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/invite-link-nc-v27-ocis-v5.yml b/.github/workflows/invite-link-nc-v27-ocis-v5.yml index a996accf..dcc80afd 100644 --- a/.github/workflows/invite-link-nc-v27-ocis-v5.yml +++ b/.github/workflows/invite-link-nc-v27-ocis-v5.yml @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: ocis, - version: 5.0.9 + version: v5.0.9 }, ] diff --git a/.github/workflows/invite-link-oc-v10-ocis-v5.yml b/.github/workflows/invite-link-oc-v10-ocis-v5.yml index a6e3ef20..05acdbc2 100644 --- a/.github/workflows/invite-link-oc-v10-ocis-v5.yml +++ b/.github/workflows/invite-link-oc-v10-ocis-v5.yml @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: ocis, - version: 5.0.9 + version: v5.0.9 }, ] diff --git a/.github/workflows/invite-link-ocis-v5-nc-v27.yml b/.github/workflows/invite-link-ocis-v5-nc-v27.yml index 45548d39..acb08b29 100644 --- a/.github/workflows/invite-link-ocis-v5-nc-v27.yml +++ b/.github/workflows/invite-link-ocis-v5-nc-v27.yml @@ -29,7 +29,7 @@ jobs: sender: [ { platform: ocis, - version: 5.0.9 + version: v5.0.9 }, ] receiver: [ diff --git a/.github/workflows/invite-link-ocis-v5-oc-v10.yml b/.github/workflows/invite-link-ocis-v5-oc-v10.yml index 1952bbca..e7c4a3fc 100644 --- a/.github/workflows/invite-link-ocis-v5-oc-v10.yml +++ b/.github/workflows/invite-link-ocis-v5-oc-v10.yml @@ -29,7 +29,7 @@ jobs: sender: [ { platform: ocis, - version: 5.0.9 + version: v5.0.9 }, ] receiver: [ diff --git a/.github/workflows/invite-link-ocis-v5-ocis-v5.yml b/.github/workflows/invite-link-ocis-v5-ocis-v5.yml index f7d297e0..72395352 100644 --- a/.github/workflows/invite-link-ocis-v5-ocis-v5.yml +++ b/.github/workflows/invite-link-ocis-v5-ocis-v5.yml @@ -29,13 +29,13 @@ jobs: sender: [ { platform: ocis, - version: 5.0.9 + version: v5.0.9 }, ] receiver: [ { platform: ocis, - version: 5.0.9 + version: v5.0.9 }, ] diff --git a/.github/workflows/login-ocis-v5.yml b/.github/workflows/login-ocis-v5.yml index e55dee4f..b087399b 100644 --- a/.github/workflows/login-ocis-v5.yml +++ b/.github/workflows/login-ocis-v5.yml @@ -29,7 +29,7 @@ jobs: efss: [ { platform: ocis, - version: 5.0.9 + version: v5.0.9 }, ] diff --git a/.github/workflows/login-ocmstub-v1.yml b/.github/workflows/login-ocmstub-v1.yml index 0326068d..514083d7 100644 --- a/.github/workflows/login-ocmstub-v1.yml +++ b/.github/workflows/login-ocmstub-v1.yml @@ -29,7 +29,7 @@ jobs: efss: [ { platform: ocmstub, - version: "1.0" + version: v1.0.0 }, ] diff --git a/.github/workflows/login-seafile-v11.yml b/.github/workflows/login-seafile-v11.yml index 2fcdb156..74e91711 100644 --- a/.github/workflows/login-seafile-v11.yml +++ b/.github/workflows/login-seafile-v11.yml @@ -29,7 +29,7 @@ jobs: efss: [ { platform: seafile, - version: 11.0.5 + version: v11.0.5 }, ] diff --git a/.github/workflows/share-link-nc-v27-os-v1.yml b/.github/workflows/share-link-nc-v27-os-v1.yml index 2ad331fa..e50c4144 100644 --- a/.github/workflows/share-link-nc-v27-os-v1.yml +++ b/.github/workflows/share-link-nc-v27-os-v1.yml @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] diff --git a/.github/workflows/share-with-nc-v27-os-v1.yml b/.github/workflows/share-with-nc-v27-os-v1.yml index b2c5df45..9632e708 100644 --- a/.github/workflows/share-with-nc-v27-os-v1.yml +++ b/.github/workflows/share-with-nc-v27-os-v1.yml @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] diff --git a/.github/workflows/share-with-nc-v28-os-v1.yml b/.github/workflows/share-with-nc-v28-os-v1.yml index 2a923629..a7c1b3a0 100644 --- a/.github/workflows/share-with-nc-v28-os-v1.yml +++ b/.github/workflows/share-with-nc-v28-os-v1.yml @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] diff --git a/.github/workflows/share-with-oc-v10-os-v1.yml b/.github/workflows/share-with-oc-v10-os-v1.yml index 505d1efd..7900a05f 100644 --- a/.github/workflows/share-with-oc-v10-os-v1.yml +++ b/.github/workflows/share-with-oc-v10-os-v1.yml @@ -35,7 +35,7 @@ jobs: receiver: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] diff --git a/.github/workflows/share-with-os-v1-nc-v27.yml b/.github/workflows/share-with-os-v1-nc-v27.yml index d4c8f1c0..083c64ad 100644 --- a/.github/workflows/share-with-os-v1-nc-v27.yml +++ b/.github/workflows/share-with-os-v1-nc-v27.yml @@ -29,7 +29,7 @@ jobs: sender: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] receiver: [ diff --git a/.github/workflows/share-with-os-v1-nc-v28.yml b/.github/workflows/share-with-os-v1-nc-v28.yml index 8d6fc699..f91a0e26 100644 --- a/.github/workflows/share-with-os-v1-nc-v28.yml +++ b/.github/workflows/share-with-os-v1-nc-v28.yml @@ -29,7 +29,7 @@ jobs: sender: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] receiver: [ diff --git a/.github/workflows/share-with-os-v1-oc-10.yml b/.github/workflows/share-with-os-v1-oc-10.yml index 507f6652..33eb35b3 100644 --- a/.github/workflows/share-with-os-v1-oc-10.yml +++ b/.github/workflows/share-with-os-v1-oc-10.yml @@ -29,7 +29,7 @@ jobs: sender: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] receiver: [ diff --git a/.github/workflows/share-with-os-v1-os-v1.yml b/.github/workflows/share-with-os-v1-os-v1.yml index 3189c260..cb0c08b0 100644 --- a/.github/workflows/share-with-os-v1-os-v1.yml +++ b/.github/workflows/share-with-os-v1-os-v1.yml @@ -29,13 +29,13 @@ jobs: sender: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] receiver: [ { platform: ocmstub, - version: "v1.0.0" + version: v1.0.0 }, ] diff --git a/.github/workflows/share-with-sf-v11-sf-v11.yml b/.github/workflows/share-with-sf-v11-sf-v11.yml index 0a3c516f..f1098b45 100644 --- a/.github/workflows/share-with-sf-v11-sf-v11.yml +++ b/.github/workflows/share-with-sf-v11-sf-v11.yml @@ -29,13 +29,13 @@ jobs: sender: [ { platform: seafile, - version: 11.0.5 + version: v11.0.5 }, ] receiver: [ { platform: seafile, - version: 11.0.5 + version: v11.0.5 }, ] From 84158b48b3069fb42b7d8d85509a58399233a9d4 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 14:33:27 +0330 Subject: [PATCH 136/184] refactor: standardize EFSS_PLATFORM_VERSION formatting across Docker scripts --- docker/pull/ocm-test-suite/nextcloud.sh | 2 +- docker/pull/ocm-test-suite/ocis.sh | 4 ++-- docker/pull/ocm-test-suite/ocmstub.sh | 4 ++-- docker/pull/ocm-test-suite/owncloud.sh | 6 +++++- docker/pull/ocm-test-suite/seafile.sh | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docker/pull/ocm-test-suite/nextcloud.sh b/docker/pull/ocm-test-suite/nextcloud.sh index f3c76af0..90c8c51e 100755 --- a/docker/pull/ocm-test-suite/nextcloud.sh +++ b/docker/pull/ocm-test-suite/nextcloud.sh @@ -13,4 +13,4 @@ docker pull mariadb:11.4.2 docker pull cypress/included:13.13.1 # dev-stock images. -docker pull "pondersource/dev-stock-nextcloud:${EFSS_PLATFORM_VERSION}" +docker pull "pondersource/nextcloud:${EFSS_PLATFORM_VERSION}" diff --git a/docker/pull/ocm-test-suite/ocis.sh b/docker/pull/ocm-test-suite/ocis.sh index 38ac0724..a2c82e98 100755 --- a/docker/pull/ocm-test-suite/ocis.sh +++ b/docker/pull/ocm-test-suite/ocis.sh @@ -5,10 +5,10 @@ set -e # oCIS version: # - 5.0.9 -EFSS_PLATFORM_VERSION=${1:-"5.0.9"} +EFSS_PLATFORM_VERSION=${1:-"V5.0.9"} # 3rd party images. docker pull cypress/included:13.13.1 # images. -docker pull "owncloud/ocis:${EFSS_PLATFORM_VERSION}" +docker pull "owncloud/ocis:${EFSS_PLATFORM_VERSION#v}" diff --git a/docker/pull/ocm-test-suite/ocmstub.sh b/docker/pull/ocm-test-suite/ocmstub.sh index 6b085b7f..f236ceb1 100755 --- a/docker/pull/ocm-test-suite/ocmstub.sh +++ b/docker/pull/ocm-test-suite/ocmstub.sh @@ -6,7 +6,7 @@ set -e # nextcloud version: # - v27.1.11 # - v28.0.14 -EFSS_PLATFORM_VERSION=${1:-"1.0"} +EFSS_PLATFORM_VERSION=${1:-"V1.0.0"} # dev-stock images. -docker pull "pondersource/dev-stock-ocmstub:${EFSS_PLATFORM_VERSION}" +docker pull "pondersource/ocmstub:${EFSS_PLATFORM_VERSION}" diff --git a/docker/pull/ocm-test-suite/owncloud.sh b/docker/pull/ocm-test-suite/owncloud.sh index b9926a7f..caf0732a 100755 --- a/docker/pull/ocm-test-suite/owncloud.sh +++ b/docker/pull/ocm-test-suite/owncloud.sh @@ -3,9 +3,13 @@ # @michielbdejong halt on error in docker init scripts. set -e +# owncloud version: +# - 10.15.0 +EFSS_PLATFORM_VERSION=${1:-"v10.15.0"} + # 3rd party images. docker pull mariadb:11.4.2 docker pull cypress/included:13.13.1 # dev-stock images. -docker pull pondersource/dev-stock-owncloud-ocm-test-suite:latest +docker pull "pondersource/owncloud:${EFSS_PLATFORM_VERSION}" diff --git a/docker/pull/ocm-test-suite/seafile.sh b/docker/pull/ocm-test-suite/seafile.sh index 0a7b039f..09725e48 100755 --- a/docker/pull/ocm-test-suite/seafile.sh +++ b/docker/pull/ocm-test-suite/seafile.sh @@ -8,10 +8,10 @@ set -e # - 9.0.10 # - 10.0.1 # - 11.0.5 -EFSS_PLATFORM_VERSION=${1:-"11.0.5"} +EFSS_PLATFORM_VERSION=${1:-"v11.0.5"} # 3rd party images. docker pull mariadb:11.4.2 docker pull memcached:1.6.18 docker pull cypress/included:13.13.1 -docker pull "seafileltd/seafile-mc:${EFSS_PLATFORM_VERSION}" +docker pull "seafileltd/seafile-mc:${EFSS_PLATFORM_VERSION#v}" From bd0499555788b2638bf4a007db604044f584e66c Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 24 Dec 2024 08:41:47 +0000 Subject: [PATCH 137/184] refactor: docker image names --- README.md | 20 +- docker/build/federatedgroups.sh | 33 -- docker/build/php-base.sh | 39 -- docker/build/sciencemesh.sh | 51 -- docker/build/solid.sh | 45 -- docker/build/sunet.sh | 34 -- docker/build/surf-token-based-access.sh | 33 -- docker/build/surf-trashbin.sh | 33 -- .../nextcloud-sciencemesh.Dockerfile | 2 +- docker/dockerfiles/nextcloud-solid.Dockerfile | 2 +- .../owncloud-federatedgroups.Dockerfile | 2 +- .../owncloud-ocm-test-suite.Dockerfile | 2 +- .../owncloud-opencloudmesh.Dockerfile | 2 +- .../owncloud-sciencemesh.Dockerfile | 2 +- .../owncloud-surf-trashbin.Dockerfile | 2 +- .../owncloud-token-based-access.Dockerfile | 2 +- docker/dockerfiles/php-base.Dockerfile | 109 ----- docker/pull/federatedgroups.sh | 9 - docker/pull/ocm-test-suite.sh | 23 - docker/pull/sciencemesh.sh | 12 - docker/pull/solid.sh | 6 - docker/pull/sunet.sh | 7 - docker/pull/surf-token-based-access.sh | 6 - docker/pull/surf-trashbin.sh | 6 - docker/push/federatedgroups.sh | 12 - docker/push/ocm-test-suite.sh | 12 - docker/push/sciencemesh.sh | 10 - docker/push/solid.sh | 8 - docker/push/sunet.sh | 9 - docker/push/surf-token-based-access.sh | 8 - docker/push/surf-trashbin.sh | 8 - docker/scripts/init/nextcloud.sh | 18 - docker/scripts/init/owncloud.sh | 16 - docker/scripts/reva/kill.sh | 7 - docker/scripts/reva/run.sh | 51 -- init/ocm-test-suite.sh | 4 +- init/sciencemesh.sh | 4 +- init/sm-sram-ocm.sh | 2 +- init/solid-nextcloud-app.sh | 2 +- init/token-based-access.sh | 2 +- release/federatedgroups-owncloud.sh | 2 +- release/ocm-owncloud.sh | 2 +- release/sciencemesh-nextcloud.sh | 4 +- release/sciencemesh-owncloud.sh | 435 +++++++++++++----- tests/sunet.sh | 2 +- tests/surf-trashbin.sh | 2 +- tests/testing-ocm-nc-oc.sh | 4 +- tests/testing-ocm.sh | 4 +- tests/token-based-access.sh | 4 +- 49 files changed, 352 insertions(+), 762 deletions(-) delete mode 100755 docker/build/federatedgroups.sh delete mode 100755 docker/build/php-base.sh delete mode 100755 docker/build/sciencemesh.sh delete mode 100755 docker/build/solid.sh delete mode 100755 docker/build/sunet.sh delete mode 100755 docker/build/surf-token-based-access.sh delete mode 100755 docker/build/surf-trashbin.sh delete mode 100644 docker/dockerfiles/php-base.Dockerfile delete mode 100755 docker/pull/federatedgroups.sh delete mode 100755 docker/pull/ocm-test-suite.sh delete mode 100755 docker/pull/sciencemesh.sh delete mode 100755 docker/pull/solid.sh delete mode 100755 docker/pull/sunet.sh delete mode 100755 docker/pull/surf-token-based-access.sh delete mode 100755 docker/pull/surf-trashbin.sh delete mode 100755 docker/push/federatedgroups.sh delete mode 100755 docker/push/ocm-test-suite.sh delete mode 100755 docker/push/sciencemesh.sh delete mode 100755 docker/push/solid.sh delete mode 100755 docker/push/sunet.sh delete mode 100755 docker/push/surf-token-based-access.sh delete mode 100755 docker/push/surf-trashbin.sh delete mode 100755 docker/scripts/init/nextcloud.sh delete mode 100755 docker/scripts/init/owncloud.sh delete mode 100755 docker/scripts/reva/kill.sh delete mode 100755 docker/scripts/reva/run.sh diff --git a/README.md b/README.md index eada09bc..ca3228b0 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ Some popular EFSS platforms include **Nextcloud** and **ownCloud**, which provid | **Repository** | **Tag** | **Branch** | **Upstream** | |----------------------------------|-----------------|---------------------------------------------------------------------------|------------------------------------------------------------------------------| -| pondersource/dev-stock-nextcloud | latest, v30.0.2 | [v30.0.2](https://github.com/nextcloud/server/releases/tag/v30.0.2) | [Official Nextcloud Server](https://github.com/nextcloud/server) | -| pondersource/dev-stock-nextcloud | v29.0.10 | [v29.0.10](https://github.com/nextcloud/server/releases/tag/v29.0.10) | [Official Nextcloud Server](https://github.com/nextcloud/server) | -| pondersource/dev-stock-nextcloud | v28.0.14 | [v28.0.14](https://github.com/nextcloud/server/releases/tag/v28.0.14) | [Official Nextcloud Server](https://github.com/nextcloud/server) | -| pondersource/dev-stock-nextcloud | v27.1.11 | [v27.1.11](https://github.com/nextcloud/server/releases/tag/v27.1.11) | [Official Nextcloud Server](https://github.com/nextcloud/server) | +| pondersource/nextcloud | latest, v30.0.2 | [v30.0.2](https://github.com/nextcloud/server/releases/tag/v30.0.2) | [Official Nextcloud Server](https://github.com/nextcloud/server) | +| pondersource/nextcloud | v29.0.10 | [v29.0.10](https://github.com/nextcloud/server/releases/tag/v29.0.10) | [Official Nextcloud Server](https://github.com/nextcloud/server) | +| pondersource/nextcloud | v28.0.14 | [v28.0.14](https://github.com/nextcloud/server/releases/tag/v28.0.14) | [Official Nextcloud Server](https://github.com/nextcloud/server) | +| pondersource/nextcloud | v27.1.11 | [v27.1.11](https://github.com/nextcloud/server/releases/tag/v27.1.11) | [Official Nextcloud Server](https://github.com/nextcloud/server) | --- @@ -60,7 +60,7 @@ Some popular EFSS platforms include **Nextcloud** and **ownCloud**, which provid | **Repository** | **Tag** | **Branch** | **Upstream** | |----------------------------------|---------------|----------------------------------------------------------------------------|------------------------------------------------------------------------------| -| pondersource/dev-stock-owncloud | v10.15.0 | [v10.15.0](https://github.com/owncloud/core/releases/tag/v10.15.0) | [Official ownCloud Core](https://github.com/owncloud/core) | +| pondersource/owncloud | v10.15.0 | [v10.15.0](https://github.com/owncloud/core/releases/tag/v10.15.0) | [Official ownCloud Core](https://github.com/owncloud/core) | #### Docker Pull Commands @@ -68,14 +68,14 @@ To pull the Docker images for EFSS, use the following commands: ```bash # Pull the latest version of Nextcloud -docker pull pondersource/dev-stock-nextcloud:latest +docker pull pondersource/nextcloud:latest # Pull a specific version of Nextcloud -docker pull pondersource/dev-stock-nextcloud:v30.0.2 -docker pull pondersource/dev-stock-nextcloud:v29.0.10 +docker pull pondersource/nextcloud:v30.0.2 +docker pull pondersource/nextcloud:v29.0.10 # Pull a specific version of ownCloud -docker pull pondersource/dev-stock-owncloud:v10.15.0 +docker pull pondersource/owncloud:v10.15.0 ``` ## Our Dockerized Reva Images 🚀 @@ -391,7 +391,7 @@ flowchart TD B --> C[Pull Docker Images] C --> C1[Pull MariaDB:11.4.2] C --> C2[Pull Cypress:13.13.1] - C --> C3[Pull pondersource/dev-stock-nextcloud:v27.1.11] + C --> C3[Pull pondersource/nextcloud:v27.1.11] %% Initialize Environment C3 --> D[Initialize Environment] diff --git a/docker/build/federatedgroups.sh b/docker/build/federatedgroups.sh deleted file mode 100755 index 1e83e07a..00000000 --- a/docker/build/federatedgroups.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} - -export DOCKER_BUILDKIT="${USE_BUILDKIT}" - -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . - -echo Building pondersource/dev-stock-php-base -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . - -echo Building pondersource/dev-stock-owncloud-opencloudmesh -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-opencloudmesh.Dockerfile --tag pondersource/dev-stock-owncloud-opencloudmesh:latest . - -echo Building pondersource/dev-stock-owncloud-federatedgroups -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-federatedgroups.Dockerfile --tag pondersource/dev-stock-owncloud-federatedgroups:latest . diff --git a/docker/build/php-base.sh b/docker/build/php-base.sh deleted file mode 100755 index 68e75113..00000000 --- a/docker/build/php-base.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} - -export DOCKER_BUILDKIT="${USE_BUILDKIT}" - -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . - -echo "Building pondersource/php-base:8.3" -docker build --build-arg CACHEBUST="default" \ - --file ./dockerfiles/php-base-8.3.Dockerfile \ - --tag "pondersource/php-base:8.3" \ - . - -echo "Building pondersource/php-base:7.4" -docker build --build-arg CACHEBUST="default" \ - --file ./dockerfiles/php-base-7.4.Dockerfile \ - --tag "pondersource/php-base:7.4" \ - . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v28.0.14" --file ./dockerfiles/nextcloud-base.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.14 --tag pondersource/dev-stock-nextcloud:latest . diff --git a/docker/build/sciencemesh.sh b/docker/build/sciencemesh.sh deleted file mode 100755 index 6e2b0dd9..00000000 --- a/docker/build/sciencemesh.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} - -export DOCKER_BUILDKIT="${USE_BUILDKIT}" - -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . - -echo Building pondersource/dev-stock-revad -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/revad.Dockerfile --tag pondersource/dev-stock-revad:latest . - -echo Building pondersource/dev-stock-php-base -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v30.0.2" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.2 --tag pondersource/dev-stock-nextcloud:latest . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v29.0.10" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.10 . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v28.0.14" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.14 . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . - -echo Building pondersource/dev-stock-nextcloud-sciencemesh -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-sciencemesh.Dockerfile --tag pondersource/dev-stock-nextcloud-sciencemesh:latest . - -echo Building pondersource/dev-stock-owncloud -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud.Dockerfile --tag pondersource/dev-stock-owncloud:latest . - -echo Building pondersource/dev-stock-owncloud-sciencemesh -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-sciencemesh.Dockerfile --tag pondersource/dev-stock-owncloud-sciencemesh:latest . diff --git a/docker/build/solid.sh b/docker/build/solid.sh deleted file mode 100755 index 76630a18..00000000 --- a/docker/build/solid.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} - -export DOCKER_BUILDKIT="${USE_BUILDKIT}" - -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . - -echo Building pondersource/dev-stock-php-base -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v30.0.2" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.2 --tag pondersource/dev-stock-nextcloud:latest . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v30.0.2" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v30.0.2 --tag pondersource/dev-stock-nextcloud:latest . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v29.0.10" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v29.0.10 . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v28.0.14" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v28.0.14 . - -echo Building pondersource/dev-stock-nextcloud -docker build --build-arg CACHEBUST="default" --build-arg NEXTCLOUD_BRANCH="v27.1.11" --file ./dockerfiles/nextcloud.Dockerfile --tag pondersource/dev-stock-nextcloud:v27.1.11 . - -echo Building pondersource/dev-stock-nextcloud-solid -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-solid.Dockerfile --tag pondersource/dev-stock-nextcloud-solid:latest . diff --git a/docker/build/sunet.sh b/docker/build/sunet.sh deleted file mode 100755 index 4b8a3668..00000000 --- a/docker/build/sunet.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} - -export DOCKER_BUILDKIT="${USE_BUILDKIT}" - -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . - -echo Building pondersource/dev-stock-nextcloud-sunet -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/nextcloud-sunet.Dockerfile --tag pondersource/dev-stock-nextcloud-sunet:latest . - -cd simple-saml-php - -echo Building pondersource/dev-stock-simple-saml-php -docker build --tag pondersource/dev-stock-simple-saml-php . - -cd .. diff --git a/docker/build/surf-token-based-access.sh b/docker/build/surf-token-based-access.sh deleted file mode 100755 index 24fcdf26..00000000 --- a/docker/build/surf-token-based-access.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} - -export DOCKER_BUILDKIT="${USE_BUILDKIT}" - -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . - -echo Building pondersource/dev-stock-php-base -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . - -echo Building pondersource/dev-stock-owncloud -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud.Dockerfile --tag pondersource/dev-stock-owncloud:latest . - -echo Building pondersource/dev-stock-owncloud-token-based-access -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-token-based-access.Dockerfile --tag pondersource/dev-stock-owncloud-token-based-access:latest . diff --git a/docker/build/surf-trashbin.sh b/docker/build/surf-trashbin.sh deleted file mode 100755 index e79c3a46..00000000 --- a/docker/build/surf-trashbin.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/.." || exit - -# use docker buildkit. you can disable buildkit by providing 0 as first argument. -USE_BUILDKIT=${1:-"1"} - -export DOCKER_BUILDKIT="${USE_BUILDKIT}" - -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . - -echo Building pondersource/dev-stock-php-base -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/php-base.Dockerfile --tag pondersource/dev-stock-php-base:latest . - -echo Building pondersource/dev-stock-owncloud -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud.Dockerfile --tag pondersource/dev-stock-owncloud:latest . - -echo Building pondersource/dev-stock-owncloud-surf-trashbin -docker build --build-arg CACHEBUST="default" --file ./dockerfiles/owncloud-surf-trashbin.Dockerfile --tag pondersource/dev-stock-owncloud-surf-trashbin:latest . diff --git a/docker/dockerfiles/nextcloud-sciencemesh.Dockerfile b/docker/dockerfiles/nextcloud-sciencemesh.Dockerfile index e1a88dfe..bbc95df9 100644 --- a/docker/dockerfiles/nextcloud-sciencemesh.Dockerfile +++ b/docker/dockerfiles/nextcloud-sciencemesh.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-nextcloud:v27.1.11 +FROM pondersource/nextcloud:v27.1.11 # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/dockerfiles/nextcloud-solid.Dockerfile b/docker/dockerfiles/nextcloud-solid.Dockerfile index b5753077..a230e09b 100644 --- a/docker/dockerfiles/nextcloud-solid.Dockerfile +++ b/docker/dockerfiles/nextcloud-solid.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-nextcloud:latest +FROM pondersource/nextcloud:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/dockerfiles/owncloud-federatedgroups.Dockerfile b/docker/dockerfiles/owncloud-federatedgroups.Dockerfile index 2a1d47f8..4eb71b01 100644 --- a/docker/dockerfiles/owncloud-federatedgroups.Dockerfile +++ b/docker/dockerfiles/owncloud-federatedgroups.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-owncloud-opencloudmesh:latest +FROM pondersource/owncloud-opencloudmesh:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/dockerfiles/owncloud-ocm-test-suite.Dockerfile b/docker/dockerfiles/owncloud-ocm-test-suite.Dockerfile index a25754e1..b9dd37e5 100644 --- a/docker/dockerfiles/owncloud-ocm-test-suite.Dockerfile +++ b/docker/dockerfiles/owncloud-ocm-test-suite.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-owncloud:latest +FROM pondersource/owncloud:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/dockerfiles/owncloud-opencloudmesh.Dockerfile b/docker/dockerfiles/owncloud-opencloudmesh.Dockerfile index 64be8956..6c924a4e 100644 --- a/docker/dockerfiles/owncloud-opencloudmesh.Dockerfile +++ b/docker/dockerfiles/owncloud-opencloudmesh.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-owncloud:latest +FROM pondersource/owncloud:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/dockerfiles/owncloud-sciencemesh.Dockerfile b/docker/dockerfiles/owncloud-sciencemesh.Dockerfile index 020e992f..1e66ffd3 100644 --- a/docker/dockerfiles/owncloud-sciencemesh.Dockerfile +++ b/docker/dockerfiles/owncloud-sciencemesh.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-owncloud:latest +FROM pondersource/owncloud:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/dockerfiles/owncloud-surf-trashbin.Dockerfile b/docker/dockerfiles/owncloud-surf-trashbin.Dockerfile index 5258bd10..5cec1cd8 100644 --- a/docker/dockerfiles/owncloud-surf-trashbin.Dockerfile +++ b/docker/dockerfiles/owncloud-surf-trashbin.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-owncloud:latest +FROM pondersource/owncloud:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/dockerfiles/owncloud-token-based-access.Dockerfile b/docker/dockerfiles/owncloud-token-based-access.Dockerfile index ce85b5ff..46c50eaa 100644 --- a/docker/dockerfiles/owncloud-token-based-access.Dockerfile +++ b/docker/dockerfiles/owncloud-token-based-access.Dockerfile @@ -1,4 +1,4 @@ -FROM pondersource/dev-stock-owncloud:latest +FROM pondersource/owncloud:latest # keys for oci taken from: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys diff --git a/docker/dockerfiles/php-base.Dockerfile b/docker/dockerfiles/php-base.Dockerfile deleted file mode 100644 index 11e7c1c9..00000000 --- a/docker/dockerfiles/php-base.Dockerfile +++ /dev/null @@ -1,109 +0,0 @@ -FROM ubuntu:22.04 - -# keys for oci taken from: -# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys -LABEL org.opencontainers.image.licenses=MIT -LABEL org.opencontainers.image.title="PonderSource Base PHP Image" -LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" -LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" - -# set timezone. -ENV TZ=UTC -RUN ln --symbolic --no-dereference --force /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -ENV DEBIAN_FRONTEND noninteractive - -RUN apt-get update --yes - -# install dependencies. -RUN apt-get install --yes \ - git \ - vim \ - curl \ - wget \ - sudo \ - unzip \ - libxml2 \ - iproute2 \ - apt-utils \ - libxml2-dev \ - lsb-release \ - mysql-client \ - build-essential \ - ca-certificates \ - apt-transport-https \ - software-properties-common - -# add the Ondrej PPA, which contains all versions of PHP packages for Ubuntu systems. -RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php -RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/apache2 - -RUN apt-get update --yes - -RUN apt-get install --yes apache2 - -# install php versions -RUN apt-get install --yes \ - php8.2 \ - php8.2-gd \ - php8.2-xml \ - php8.2-zip \ - php8.2-curl \ - php8.2-intl \ - php8.2-redis \ - php8.2-mysql \ - php8.2-xdebug \ - php8.2-opcache \ - php8.2-sqlite3 \ - php8.2-mbstring - -RUN apt-get install --yes \ - php7.4 \ - php7.4-gd \ - php7.4-xml \ - php7.4-zip \ - php7.4-apcu \ - php7.4-curl \ - php7.4-intl \ - php7.4-json \ - php7.4-redis \ - php7.4-mysql \ - php7.4-xdebug \ - php7.4-imagick \ - php7.4-opcache \ - php7.4-sqlite3 \ - php7.4-mbstring \ - php7.4-memcached - - -# PHP switcher script. -COPY ./scripts/switch-php.sh /usr/bin/switch-php.sh -RUN chmod +x /usr/bin/switch-php.sh - -# copy xdebug configuration and create its link in each PHP version conf directory. -COPY ./configs/20-xdebug.ini /configs-pondersource/20-xdebug.ini -RUN ln --symbolic --force /configs-pondersource/20-xdebug.ini /etc/php/7.4/cli/conf.d/20-xdebug.ini -RUN ln --symbolic --force /configs-pondersource/20-xdebug.ini /etc/php/8.2/cli/conf.d/20-xdebug.ini - -# apache config. -COPY ./configs/site.conf /configs-pondersource/site.conf -RUN ln --symbolic --force /configs-pondersource/site.conf /etc/apache2/sites-enabled/000-default.conf - -# trust all the certificates: -COPY ./tls/certificates/* /tls/ -COPY ./tls/certificate-authority/* /tls/ -RUN ln --symbolic --force /tls/*.crt /usr/local/share/ca-certificates -RUN update-ca-certificates -RUN a2enmod ssl - -# app directory. -WORKDIR /var/www -RUN chown www-data:www-data . - -EXPOSE 443 - -COPY ./scripts/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] -CMD /usr/sbin/apache2ctl -DFOREGROUND diff --git a/docker/pull/federatedgroups.sh b/docker/pull/federatedgroups.sh deleted file mode 100755 index 1f7278c5..00000000 --- a/docker/pull/federatedgroups.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -docker pull pondersource/dev-stock-php-base:latest -docker pull pondersource/dev-stock-owncloud:latest -docker pull pondersource/dev-stock-owncloud-opencloudmesh:latest -docker pull pondersource/dev-stock-owncloud-federatedgroups:latest diff --git a/docker/pull/ocm-test-suite.sh b/docker/pull/ocm-test-suite.sh deleted file mode 100755 index 17589ad2..00000000 --- a/docker/pull/ocm-test-suite.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# 3rd party images. -docker pull mariadb:11.4.2 -docker pull memcached:1.6.18 -docker pull collabora/code:latest -docker pull cypress/included:13.13.1 -docker pull cs3org/wopiserver:latest -docker pull seafileltd/seafile-mc:11.0.5 - -# dev-stock images. -docker pull pondersource/dev-stock-revad:latest -docker pull pondersource/dev-stock-ocmstub:latest -docker pull pondersource/dev-stock-nextcloud:v30.0.2 -docker pull pondersource/dev-stock-nextcloud:v39.0.8 -docker pull pondersource/dev-stock-nextcloud:v28.0.14 -docker pull pondersource/dev-stock-nextcloud:v27.1.11 -docker pull pondersource/dev-stock-nextcloud-sciencemesh:latest -docker pull pondersource/dev-stock-owncloud-sciencemesh:latest -docker pull pondersource/dev-stock-owncloud-ocm-test-suite:latest diff --git a/docker/pull/sciencemesh.sh b/docker/pull/sciencemesh.sh deleted file mode 100755 index cdec8439..00000000 --- a/docker/pull/sciencemesh.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -docker pull pondersource/dev-stock-revad:latest -docker pull pondersource/dev-stock-nextcloud:v30.0.2 -docker pull pondersource/dev-stock-nextcloud:v39.0.8 -docker pull pondersource/dev-stock-nextcloud:v28.0.14 -docker pull pondersource/dev-stock-nextcloud:v27.1.11 -docker pull pondersource/dev-stock-nextcloud-sciencemesh:latest -docker pull pondersource/dev-stock-owncloud-sciencemesh:latest diff --git a/docker/pull/solid.sh b/docker/pull/solid.sh deleted file mode 100755 index 3dd10912..00000000 --- a/docker/pull/solid.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -docker pull pondersource/dev-stock-nextcloud-solid:latest diff --git a/docker/pull/sunet.sh b/docker/pull/sunet.sh deleted file mode 100755 index 5017b4f5..00000000 --- a/docker/pull/sunet.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# docker pull pondersource/dev-stock-nextcloud-sunet:latest -# docker pull pondersource/dev-stock-simple-saml-php:latest diff --git a/docker/pull/surf-token-based-access.sh b/docker/pull/surf-token-based-access.sh deleted file mode 100755 index e0af02b5..00000000 --- a/docker/pull/surf-token-based-access.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -docker pull pondersource/dev-stock-owncloud-token-based-access:latest diff --git a/docker/pull/surf-trashbin.sh b/docker/pull/surf-trashbin.sh deleted file mode 100755 index a4c9703c..00000000 --- a/docker/pull/surf-trashbin.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -docker pull pondersource/dev-stock-owncloud-surf-trashbin:latest diff --git a/docker/push/federatedgroups.sh b/docker/push/federatedgroups.sh deleted file mode 100755 index 345a83be..00000000 --- a/docker/push/federatedgroups.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -echo "Log in as pondersource" -docker login - -docker push pondersource/dev-stock-php-base:latest -docker push pondersource/dev-stock-owncloud:latest -docker push pondersource/dev-stock-owncloud-opencloudmesh:latest -docker push pondersource/dev-stock-owncloud-federatedgroups:latest diff --git a/docker/push/ocm-test-suite.sh b/docker/push/ocm-test-suite.sh deleted file mode 100755 index f987aaba..00000000 --- a/docker/push/ocm-test-suite.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e - -echo "Log in as pondersource" -docker login - -docker push pondersource/dev-stock-revad:latest -docker push pondersource/dev-stock-nextcloud-sciencemesh:latest -docker push pondersource/dev-stock-owncloud-sciencemesh:latest -docker push pondersource/dev-stock-owncloud-ocm-test-suite:latest -docker push pondersource/dev-stock-ocmstub:1.0 \ No newline at end of file diff --git a/docker/push/sciencemesh.sh b/docker/push/sciencemesh.sh deleted file mode 100755 index b2a7fc74..00000000 --- a/docker/push/sciencemesh.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -e - -echo "Log in as pondersource" -docker login - -docker push pondersource/dev-stock-revad:latest -docker push pondersource/dev-stock-nextcloud-sciencemesh:latest -docker push pondersource/dev-stock-owncloud-sciencemesh:latest diff --git a/docker/push/solid.sh b/docker/push/solid.sh deleted file mode 100755 index 28469361..00000000 --- a/docker/push/solid.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -echo "Log in as pondersource" -docker login - -docker push pondersource/dev-stock-nextcloud-solid:latest diff --git a/docker/push/sunet.sh b/docker/push/sunet.sh deleted file mode 100755 index 7ef3d9e0..00000000 --- a/docker/push/sunet.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -e - -echo "Log in as pondersource" -docker login - -# docker push pondersource/dev-stock-nextcloud-sunet:latest -# docker push pondersource/dev-stock-simple-saml-php:latest diff --git a/docker/push/surf-token-based-access.sh b/docker/push/surf-token-based-access.sh deleted file mode 100755 index 024755e5..00000000 --- a/docker/push/surf-token-based-access.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -echo "Log in as pondersource" -docker login - -docker push pondersource/dev-stock-owncloud-token-based-access:latest diff --git a/docker/push/surf-trashbin.sh b/docker/push/surf-trashbin.sh deleted file mode 100755 index 0c663aab..00000000 --- a/docker/push/surf-trashbin.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -echo "Log in as pondersource" -docker login - -docker push pondersource/dev-stock-owncloud-surf-trashbin:latest diff --git a/docker/scripts/init/nextcloud.sh b/docker/scripts/init/nextcloud.sh deleted file mode 100755 index 27e34de3..00000000 --- a/docker/scripts/init/nextcloud.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts -set -e - -php console.php maintenance:install --admin-user "$USER" --admin-pass "$PASS" --database "mysql" \ - --database-name "efss" --database-user "root" --database-host "$DBHOST" \ - --database-pass "eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" -php console.php app:disable firstrunwizard - -# change/add lines in config.php -sed -i "3 i\ 'allow_local_remote_servers' => true," /var/www/html/config/config.php -sed -i "8 i\ 1 => 'nc1.docker'," /var/www/html/config/config.php -sed -i "9 i\ 2 => 'nc2.docker'," /var/www/html/config/config.php -sed -i "10 i\ 3 => 'nextcloud1.docker'," /var/www/html/config/config.php -sed -i "11 i\ 4 => 'nextcloud2.docker'," /var/www/html/config/config.php -sed -i "12 i\ 5 => 'nextcloud3.docker'," /var/www/html/config/config.php -sed -i "13 i\ 6 => 'nextcloud4.docker'," /var/www/html/config/config.php diff --git a/docker/scripts/init/owncloud.sh b/docker/scripts/init/owncloud.sh deleted file mode 100755 index 2ee5ba1e..00000000 --- a/docker/scripts/init/owncloud.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts -set -e - -php console.php maintenance:install --admin-user "${USER}" --admin-pass "${PASS}" --database "mysql" \ - --database-name "efss" --database-user "root" --database-host "$DBHOST" \ - --database-pass "eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" -php console.php app:disable firstrunwizard - -# change/add lines in config.php -sed -i "8 i\ 1 => 'oc1.docker'," /var/www/html/config/config.php -sed -i "9 i\ 2 => 'oc2.docker'," /var/www/html/config/config.php -sed -i "10 i\ 3 => 'owncloud1.docker'," /var/www/html/config/config.php -sed -i "11 i\ 4 => 'owncloud2.docker'," /var/www/html/config/config.php -sed -i "12 i\ 5 => (isset(\$_SERVER['HTTP_HOST']) ? \$_SERVER['HTTP_HOST'] : 'localhost')," /var/www/html/config/config.php diff --git a/docker/scripts/reva/kill.sh b/docker/scripts/reva/kill.sh deleted file mode 100755 index fac113ff..00000000 --- a/docker/scripts/reva/kill.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# kill running revad. -REVAD_PID=$(pgrep -f "revad" | tail -1) && kill -9 "${REVAD_PID}" diff --git a/docker/scripts/reva/run.sh b/docker/scripts/reva/run.sh deleted file mode 100755 index 9d278e56..00000000 --- a/docker/scripts/reva/run.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# create reva directory if it doesn't exist. -if [ ! -d /reva ]; then - mkdir -p /reva -fi - -# if /reva has any files in it, do not copy image binaries into it. -if [ -n "$(find /reva -prune -empty -type d 2>/dev/null)" ]; then - echo "/reva is an empty directory, populating it with reva binaries." - # populate /reva with Reva binaries. - cp -ar /reva-git/cmd /reva -else - ls -lsa /reva - echo "/reva contains files, doing noting." -fi - -# create new dir and copy relevant configs there. -rm -rf /etc/revad -cp -r /configs/revad /etc/revad - -# substitute placeholders and "external" values with valid ones for the testnet. -sed -i "s/your.revad.org/${HOST}.docker/" /etc/revad/*.toml -sed -i "s/localhost/${HOST}.docker/" /etc/revad/*.toml -sed -i "s/your.efss.org/${HOST//reva/}.docker/" /etc/revad/*.toml -sed -i "s/your.nginx.org/${HOST//reva/}.docker/" /etc/revad/*.toml - -# update OS certificate store. -mkdir -p /tls - -[ -d "/certificates" ] && \ - cp -f /certificates/*.crt /tls/ \ - && \ - cp -f /certificates/*.key /tls/ - -[ -d "/certificate-authority" ] && \ - cp -f /certificate-authority/*.crt /tls/ \ - && \ - cp -f /certificate-authority/*.key /tls/ - -cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true -update-ca-certificates - -ln -sf "/tls/${HOST}.crt" /tls/server.crt -ln -sf "/tls/${HOST}.key" /tls/server.key - -# run revad. -revad --dev-dir "/etc/revad" & diff --git a/init/ocm-test-suite.sh b/init/ocm-test-suite.sh index 32b42eb1..b6120ee3 100755 --- a/init/ocm-test-suite.sh +++ b/init/ocm-test-suite.sh @@ -102,7 +102,7 @@ fi redirect_to_null_cmd docker run --rm \ -v "$(pwd)/nextcloud-sciencemesh:/var/www/html/apps/sciencemesh" \ --workdir /var/www/html/apps/sciencemesh \ - pondersource/dev-stock-nextcloud-sciencemesh \ + pondersource/nextcloud-sciencemesh \ make composer # move app to its place inside efss and create symbolic links. @@ -121,7 +121,7 @@ fi redirect_to_null_cmd docker run --rm \ -v "$(pwd)/owncloud-sciencemesh:/var/www/html/apps/sciencemesh" \ --workdir /var/www/html/apps/sciencemesh \ - pondersource/dev-stock-owncloud-sciencemesh \ + pondersource/owncloud-sciencemesh \ composer install [ ! -d "owncloud/apps/sciencemesh" ] && \ diff --git a/init/sciencemesh.sh b/init/sciencemesh.sh index 0a5b7811..daaa7acc 100755 --- a/init/sciencemesh.sh +++ b/init/sciencemesh.sh @@ -49,7 +49,7 @@ BRANCH_REVA=v1.28.0 docker run -it --rm \ -v "$(pwd)/nextcloud-sciencemesh:/var/www/html/apps/sciencemesh" \ --workdir /var/www/html/apps/sciencemesh \ - pondersource/dev-stock-nextcloud-sciencemesh \ + pondersource/nextcloud-sciencemesh \ make composer # move app to its place inside efss and create symbolic links. @@ -74,7 +74,7 @@ BRANCH_REVA=v1.28.0 docker run -it --rm \ -v "$(pwd)/owncloud-sciencemesh:/var/www/html/apps/sciencemesh" \ --workdir /var/www/html/apps/sciencemesh \ - pondersource/dev-stock-owncloud-sciencemesh \ + pondersource/owncloud-sciencemesh \ composer install [ ! -d "owncloud/apps/sciencemesh" ] && \ diff --git a/init/sm-sram-ocm.sh b/init/sm-sram-ocm.sh index 6942de8b..f64c992b 100755 --- a/init/sm-sram-ocm.sh +++ b/init/sm-sram-ocm.sh @@ -54,7 +54,7 @@ BRANCH_RD_SRAM=compatibility-with-sciencemesh docker run -it --rm \ -v "$(pwd)/owncloud-sciencemesh:/var/www/html/apps/sciencemesh" \ --workdir /var/www/html/apps/sciencemesh \ - pondersource/dev-stock-owncloud-sciencemesh \ + pondersource/owncloud-sciencemesh \ composer install [ ! -d "owncloud/apps/sciencemesh" ] && \ diff --git a/init/solid-nextcloud-app.sh b/init/solid-nextcloud-app.sh index 56f7391c..cacd4486 100755 --- a/init/solid-nextcloud-app.sh +++ b/init/solid-nextcloud-app.sh @@ -50,7 +50,7 @@ BRANCH_SOLID=main docker run -it --rm \ -v "${ENV_ROOT}/nextcloud/apps/solid:/var/www/html/apps/solid" \ --workdir /var/www/html/apps/solid \ - "pondersource/dev-stock-nextcloud-solid" \ + "pondersource/nextcloud-solid" \ bash -c "composer install" \ && \ cd ../.. diff --git a/init/token-based-access.sh b/init/token-based-access.sh index e4b69349..4694aae9 100755 --- a/init/token-based-access.sh +++ b/init/token-based-access.sh @@ -40,7 +40,7 @@ BRANCH_OPENID=master docker run -it \ -v "$(pwd)/open-id-connect:/var/www/html/apps/openidconnect" \ --workdir /var/www/html/apps/openidconnect \ - pondersource/dev-stock-owncloud-rc-mounts \ + pondersource/owncloud-rc-mounts \ make install-deps docker network inspect testnet >/dev/null 2>&1 || docker network create testnet diff --git a/release/federatedgroups-owncloud.sh b/release/federatedgroups-owncloud.sh index 8dd49bcf..2f48b1df 100755 --- a/release/federatedgroups-owncloud.sh +++ b/release/federatedgroups-owncloud.sh @@ -46,7 +46,7 @@ docker run --detach --network=testnet -v "${REPO_ROOT}/temp/oc-rd-sram.sh:/init.sh" \ -v "${REPO_ROOT}/rd-sram-release:/var/www/html/apps/rd-sram-integration" \ -v "${REPO_ROOT}/release/federatedgroups.key:/var/www/federatedgroups.key" \ - pondersource/dev-stock-owncloud-rd-sram + pondersource/owncloud-rd-sram docker exec --user root oc-release.docker bash -c "chown www-data:www-data -R /var/www/html/apps/rd-sram-integration && chown www-data:www-data /var/www/federatedgroups.key" diff --git a/release/ocm-owncloud.sh b/release/ocm-owncloud.sh index b27142cc..ef3cf609 100755 --- a/release/ocm-owncloud.sh +++ b/release/ocm-owncloud.sh @@ -47,7 +47,7 @@ docker run --detach --network=testnet -v "${REPO_ROOT}/temp/oc-opencloudmesh.sh:/init.sh" \ -v "${REPO_ROOT}/oc-ocm-release:/var/www/html/apps/oc-opencloudmesh" \ -v "${REPO_ROOT}/release/opencloudmesh.key:/var/www/opencloudmesh.key" \ - pondersource/dev-stock-owncloud-opencloudmesh + pondersource/owncloud-opencloudmesh docker exec --user root oc-release.docker bash -c "chown www-data:www-data -R /var/www/html/apps/oc-opencloudmesh && chown www-data:www-data /var/www/opencloudmesh.key" diff --git a/release/sciencemesh-nextcloud.sh b/release/sciencemesh-nextcloud.sh index 3e39b415..4c484bf6 100755 --- a/release/sciencemesh-nextcloud.sh +++ b/release/sciencemesh-nextcloud.sh @@ -16,7 +16,7 @@ BRANCH_OWNCLOUD_APP=nextcloud cp -f ./docker/scripts/init/nextcloud-sciencemesh.sh ./temp/nc.sh # add additional tagging for docker images. -docker tag pondersource/dev-stock-nextcloud-sciencemesh pondersource/dev-stock-nc1-sciencemesh +docker tag pondersource/nextcloud-sciencemesh pondersource/dev-stock-nc1-sciencemesh # ownCloud Sciencemesh source code. [ ! -d "nc-sciencemesh-release" ] && \ @@ -53,7 +53,7 @@ docker run --detach --network=testnet -v "${REPO_ROOT}/temp/nc.sh:/nc-init.sh" \ -v "${REPO_ROOT}/nc-sciencemesh-release:/var/www/html/apps/sciencemesh" \ -v "${REPO_ROOT}/release/sciencemesh.key:/var/www/sciencemesh.key" \ - "pondersource/dev-stock-nextcloud-sciencemesh" + "pondersource/nextcloud-sciencemesh" docker exec --user root nc-release.docker bash -c "chown www-data:www-data -R /var/www/html/apps/sciencemesh \ && \ diff --git a/release/sciencemesh-owncloud.sh b/release/sciencemesh-owncloud.sh index a1991694..a526a1bb 100755 --- a/release/sciencemesh-owncloud.sh +++ b/release/sciencemesh-owncloud.sh @@ -1,122 +1,317 @@ #!/usr/bin/env bash -set -e - -REPO_ROOT=$(pwd) - -"${REPO_ROOT}/scripts/clean.sh" - -# repositories and branches. -REPO_OWNCLOUD_APP=https://github.com/sciencemesh/nc-sciencemesh -BRANCH_OWNCLOUD_APP=owncloud - -[ ! -d "temp" ] && mkdir -p temp - -# copy init file. -cp -f ./docker/scripts/init/owncloud-sciencemesh.sh ./temp/oc.sh - -# add additional tagging for docker images. -docker tag pondersource/dev-stock-owncloud-sciencemesh pondersource/dev-stock-oc1-sciencemesh - -# ownCloud Sciencemesh source code. -[ ! -d "oc-sciencemesh-release" ] && \ - git clone \ - --depth 1 \ - --branch ${BRANCH_OWNCLOUD_APP} \ - ${REPO_OWNCLOUD_APP} \ - oc-sciencemesh-release \ - && \ - docker run -it \ - -v "${REPO_ROOT}/oc-sciencemesh-release:/var/www/html/apps/sciencemesh" \ - --workdir /var/www/html/apps/sciencemesh \ - pondersource/dev-stock-oc1-sciencemesh \ - make composer - -"${REPO_ROOT}/release/tag-release.py" oc none oc-sciencemesh-release - -docker run --detach --network=testnet \ - --name=maria1.docker \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - -docker run --detach --network=testnet \ - --name=oc-release.docker \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="oc1" \ - -e DBHOST="maria1.docker" \ - -e USER="einstein" \ - -e PASS="relativity" \ - -v "${REPO_ROOT}/temp/oc.sh:/oc-init.sh" \ - -v "${REPO_ROOT}/oc-sciencemesh-release:/var/www/html/apps/sciencemesh" \ - -v "${REPO_ROOT}/release/sciencemesh.key:/var/www/sciencemesh.key" \ - "pondersource/dev-stock-owncloud-sciencemesh" - -docker exec --user root oc-release.docker bash -c "chown www-data:www-data -R /var/www/html/apps/sciencemesh \ - && \ - chown www-data:www-data /var/www/sciencemesh.key" - -docker exec --user www-data oc-release.docker bash -c "cd /var/www/html/apps/sciencemesh \ - && \ - mkdir -p build/sciencemesh \ - && \ - rm -rf build/sciencemesh/* \ - && \ - cp -r appinfo build/sciencemesh/ \ - && \ - cp -r css build/sciencemesh/ \ - && \ - cp -r img build/sciencemesh/ \ - && \ - cp -r js build/sciencemesh/ \ - && \ - cp -r lib build/sciencemesh/ \ - && \ - cp -r templates build/sciencemesh/ \ - && \ - cp -r composer.* build/sciencemesh/ \ - && \ - cd build/sciencemesh/ \ - && \ - composer install \ - && \ - cd /var/www/html \ - && \ - ./occ integrity:sign-app \ - --privateKey=/var/www/sciencemesh.key \ - --certificate=apps/sciencemesh/sciencemesh.crt \ - --path=apps/sciencemesh/build/sciencemesh \ - && \ - cd apps/sciencemesh/build \ - && \ - tar -cf sciencemesh.tar sciencemesh" - -docker exec --user root oc-release.docker bash -c "mkdir -p /var/www/html/apps/sciencemesh/release \ - && \ - cd /var/www/html/apps/sciencemesh/release \ - && \ - mv ../build/sciencemesh.tar . \ - && \ - rm -f -- sciencemesh.tar.gz \ - && \ - gzip sciencemesh.tar" - -"${REPO_ROOT}/scripts/clean.sh" - -# clear contents of sciencemesh key. -sudo chown gitpod:gitpod "${REPO_ROOT}/release/sciencemesh.key" -truncate -s 0 "${REPO_ROOT}/release/sciencemesh.key" - -# add new tar.gz to git and push. -sudo chown gitpod:gitpod -R "${REPO_ROOT}/oc-sciencemesh-release" -cd "${REPO_ROOT}/oc-sciencemesh-release" -git add "${REPO_ROOT}/oc-sciencemesh-release/release/sciencemesh.tar.gz" -git commit -m "Update release tarball of the application" -git push origin - -# remove the release folder. -cd "${REPO_ROOT}" -sudo rm -rf "${REPO_ROOT}/oc-sciencemesh-release" +# ----------------------------------------------------------------------------------- +# Script to Automate the Release of Sciencemesh Application for ownCloud +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script automates the process of preparing and releasing the Sciencemesh application for ownCloud. +# It handles Docker container setup, builds the application, signs it, and uploads the release tarball to GitHub. + +# Usage: +# ./sciencemesh-owncloud.sh + +# Prerequisites: +# - Docker must be installed and accessible. +# - GitHub repository must be accessible with an API token stored in the environment variable `GITHUB_TOKEN`. +# - Required repositories, keys, and scripts must be available. +# - User must have sufficient permissions to execute Docker and modify required files. +# - Ensure that `jq` is installed for JSON parsing. + +# Exit immediately on any error, treat unset variables as an error, and catch errors in pipelines. +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Configuration +# ----------------------------------------------------------------------------------- + +# Repository and branch for the ownCloud Sciencemesh app +REPO_OWNCLOUD_APP="https://github.com/sciencemesh/nc-sciencemesh" +BRANCH_OWNCLOUD_APP="owncloud" + +# Docker images +MARIADB_IMAGE="mariadb:11.4.2" +OC_IMAGE="pondersource/owncloud-sciencemesh" +COMPOSER_IMAGE="pondersource/dev-stock-oc1-sciencemesh" + +# Paths and filenames +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMP_DIR="${REPO_ROOT}/temp" +OC_RELEASE_DIR="${REPO_ROOT}/oc-sciencemesh-release" +INIT_SCRIPT_SOURCE="${REPO_ROOT}/docker/scripts/init/owncloud-sciencemesh.sh" +INIT_SCRIPT_DEST="${TEMP_DIR}/oc.sh" +TARBALL_DIR="${OC_RELEASE_DIR}/release" +TARBALL_NAME="sciencemesh.tar.gz" +TARBALL_PATH="${TARBALL_DIR}/${TARBALL_NAME}" + +# GitHub release details +GITHUB_REPO="sciencemesh/nc-sciencemesh" # Update to your GitHub repository + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose: Resolves the absolute path of the script's directory, handling symlinks. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + local dir + while [ -L "$source" ]; do + dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)" + source="$(readlink "$source")" + [[ "$source" != /* ]] && source="$dir/$source" # Resolve relative symlink + done + dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)" + printf "%s" "$dir" +} + +# ----------------------------------------------------------------------------------- +# Functions +# ----------------------------------------------------------------------------------- + +# Function: Print an error message to stderr and exit with failure code. +print_error() { + local message="$1" + printf "Error: %s\n" "$message" >&2 + exit 1 +} + +# Function: Ensure a directory exists; create it if it does not. +ensure_directory_exists() { + local dir="$1" + if [ ! -d "$dir" ]; then + mkdir -p "$dir" || print_error "Failed to create directory '$dir'." + fi +} + +# Function: Clean up temporary resources. +run_cleanup() { + printf "Cleaning up resources...\n" + if [ -x "${REPO_ROOT}/scripts/clean.sh" ]; then + "${REPO_ROOT}/scripts/clean.sh" + fi + sudo chown -R "$(whoami):$(whoami)" "${OC_RELEASE_DIR}" 2>/dev/null || true + sudo rm -rf "${OC_RELEASE_DIR}" 2>/dev/null || true + sudo rm -rf "${TEMP_DIR}" 2>/dev/null || true +} + +# Function: Create a GitHub release and upload the tarball. +create_github_release() { + local repo="$1" + local tag="$2" + local name="$3" + local file_path="$4" + + # Check for the required GitHub token. + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + print_error "GITHUB_TOKEN environment variable is not set. It is required to create a GitHub release." + fi + + # Step 1: Create the release. + printf "Creating GitHub release '%s' with tag '%s'...\n" "${name}" "${tag}" + local release_response + release_response=$(curl -s -X POST "https://api.github.com/repos/${repo}/releases" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg tag_name "$tag" \ + --arg name "$name" \ + --arg body "Automated release of the Sciencemesh application for ownCloud." \ + '{ tag_name: $tag_name, name: $name, body: $body, draft: false, prerelease: false }')") + + # Extract the upload URL from the release response. + local upload_url + upload_url=$(echo "${release_response}" | jq -r '.upload_url' | sed -e "s/{?name,label}//") + if [[ -z "${upload_url}" || "${upload_url}" == "null" ]]; then + print_error "Failed to create GitHub release. Response: ${release_response}" + fi + + # Step 2: Upload the tarball to the release. + printf "Uploading tarball '%s' to GitHub release...\n" "${file_path}" + local upload_response + upload_response=$(curl -s --data-binary @"${file_path}" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/gzip" \ + "${upload_url}?name=$(basename "${file_path}")") + + # Check if the upload was successful. + if [[ $(echo "${upload_response}" | jq -r '.id') == "null" ]]; then + print_error "Failed to upload tarball. Response: ${upload_response}" + fi + + printf "Release '%s' created and tarball uploaded successfully.\n" "${name}" +} + +# Function: Check if a command exists. +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function: Get the latest Git tag from the repository. +get_latest_git_tag() { + local repo_dir="$1" + local latest_tag + + cd "$repo_dir" || print_error "Failed to access directory '$repo_dir'." + + # Fetch all tags from the remote repository. + git fetch --tags || print_error "Failed to fetch tags from the remote repository." + + # Get the latest tag based on version sort. + latest_tag=$(git tag | sort -V | tail -n1) + + if [[ -z "$latest_tag" ]]; then + print_error "No tags found in the repository." + fi + + printf "%s" "$latest_tag" +} + +# ----------------------------------------------------------------------------------- +# Main script logic encapsulated in a function. +# ----------------------------------------------------------------------------------- +main() { + # Ensure required commands are available. + for cmd in docker git curl jq; do + if ! command_exists "$cmd"; then + print_error "Required command '$cmd' is not available. Please install it and try again." + fi + done + + # Trap to ensure cleanup is run on script exit. + trap run_cleanup EXIT + + # Step 1: Run the cleanup script at the beginning. + if [ -x "${REPO_ROOT}/scripts/clean.sh" ]; then + "${REPO_ROOT}/scripts/clean.sh" + else + print_error "Cleanup script not found or not executable at '${REPO_ROOT}/scripts/clean.sh'." + fi + + # Step 2: Create the temporary directory. + ensure_directory_exists "${TEMP_DIR}" + + # Step 3: Copy the initialization file to the temp directory. + if [ -f "${INIT_SCRIPT_SOURCE}" ]; then + cp -f "${INIT_SCRIPT_SOURCE}" "${INIT_SCRIPT_DEST}" + else + print_error "Initialization script not found at '${INIT_SCRIPT_SOURCE}'." + fi + + # Step 4: Tag the Docker image. + printf "Tagging Docker image '%s' as '%s'...\n" "${OC_IMAGE}" "${COMPOSER_IMAGE}" + docker tag "${OC_IMAGE}" "${COMPOSER_IMAGE}" || print_error "Failed to tag Docker image." + + # Step 5: Clone the Sciencemesh source code repository if not already present. + if [ ! -d "${OC_RELEASE_DIR}" ]; then + printf "Cloning Sciencemesh repository...\n" + git clone --branch "${BRANCH_OWNCLOUD_APP}" "${REPO_OWNCLOUD_APP}" "${OC_RELEASE_DIR}" || print_error "Failed to clone repository." + + # Navigate to the cloned repository directory. + cd "${OC_RELEASE_DIR}" || print_error "Failed to access directory '${OC_RELEASE_DIR}'." + + # Run `composer` using the composer Docker image. + printf "Running 'composer' inside Docker to build the application...\n" + docker run --rm \ + -v "${OC_RELEASE_DIR}:/var/www/html/apps/sciencemesh" \ + --workdir /var/www/html/apps/sciencemesh \ + "${COMPOSER_IMAGE}" \ + make composer || print_error "Failed to run 'composer' inside Docker." + fi + + # Step 6: Tag the release using a Python script. + if [ -x "${REPO_ROOT}/release/tag-release.py" ]; then + printf "Tagging the release using the Python script...\n" + "${REPO_ROOT}/release/tag-release.py" oc "${RELEASE_TAG}" "${OC_RELEASE_DIR}" || print_error "Failed to tag the release." + else + print_error "Tagging script not found or not executable at '${REPO_ROOT}/release/tag-release.py'." + fi + + # Fetch all tags to ensure they are available. + git fetch --tags || print_error "Failed to fetch tags from the repository." + + # Step 7: Get the latest Git tag from the repository. + RELEASE_TAG=$(get_latest_git_tag "${OC_RELEASE_DIR}") + RELEASE_NAME="ownCloud Sciencemesh Release ${RELEASE_TAG}" + + printf "Latest Git tag obtained: '%s'\n" "${RELEASE_TAG}" + + # Step 8: Set up the MariaDB container. + docker run --detach --network=testnet \ + --name=maria1.docker \ + -e MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" \ + "${MARIADB_IMAGE}" \ + --transaction-isolation=READ-COMMITTED \ + --binlog-format=ROW \ + --innodb-file-per-table=1 \ + --skip-innodb-read-only-compressed + + # Step 9: Set up the ownCloud container. + docker run --detach --network=testnet \ + --name=oc-release.docker \ + -e HOST="oc1" \ + -e DBHOST="maria1.docker" \ + -e USER="einstein" \ + -e PASS="relativity" \ + -v "${REPO_ROOT}/temp/oc.sh:/oc-init.sh" \ + -v "${REPO_ROOT}/oc-sciencemesh-release:/var/www/html/apps/sciencemesh" \ + -v "${REPO_ROOT}/release/sciencemesh.key:/var/www/sciencemesh.key" \ + "${OC_IMAGE}" + + # Step 10: Adjust file permissions inside the container. + docker exec --user root oc-release.docker bash -c \ + "chown www-data:www-data -R /var/www/html/apps/sciencemesh && \ + chown www-data:www-data /var/www/sciencemesh.key" + + # Step 11: Build and sign the Sciencemesh app inside the container. + docker exec --user www-data oc-release.docker bash -c \ + "cd /var/www/html/apps/sciencemesh && \ + mkdir -p build/sciencemesh && \ + rm -rf build/sciencemesh/* && \ + cp -r appinfo css img js lib templates composer.* build/sciencemesh/ && \ + cd build/sciencemesh && \ + composer install && \ + cd /var/www/html && \ + ./occ integrity:sign-app \ + --privateKey=/var/www/sciencemesh.key \ + --certificate=apps/sciencemesh/sciencemesh.crt \ + --path=apps/sciencemesh/build/sciencemesh && \ + cd apps/sciencemesh/build && \ + tar -cf sciencemesh.tar sciencemesh" + + # Step 12: Compress and move the tarball. + docker exec --user root oc-release.docker bash -c \ + "mkdir -p /var/www/html/apps/sciencemesh/release && \ + cd /var/www/html/apps/sciencemesh/release && \ + mv ../build/sciencemesh.tar . && \ + rm -f -- sciencemesh.tar.gz && \ + gzip sciencemesh.tar" + + # Step 13: Build and compress the application tarball inside Docker. + printf "Building and compressing the application tarball inside Docker...\n" + # Ensure the container is running + if ! docker ps --format '{{.Names}}' | grep -q "^oc-release.docker$"; then + print_error "Docker container 'oc-release.docker' is not running." + fi + + docker exec --user root oc-release.docker bash -c \ + "cd /var/www/html/apps/sciencemesh/release && \ + mv ../build/sciencemesh.tar . && \ + rm -f -- sciencemesh.tar.gz && \ + gzip sciencemesh.tar" || print_error "Failed to build and compress the tarball." + + # Step 14: Verify the tarball exists. + if [ ! -f "${TARBALL_PATH}" ]; then + print_error "Tarball not found at '${TARBALL_PATH}'." + fi + + # Step 15: Upload the tarball to GitHub as a release. + create_github_release "${GITHUB_REPO}" "${RELEASE_TAG}" "${RELEASE_NAME}" "${TARBALL_PATH}" + + # Step 16: Clean up resources. + run_cleanup +} + +# ----------------------------------------------------------------------------------- +# Execute the main function +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/tests/sunet.sh b/tests/sunet.sh index 9cbef687..2225213b 100755 --- a/tests/sunet.sh +++ b/tests/sunet.sh @@ -66,7 +66,7 @@ docker run --detach --network=testnet -v "${ENV_ROOT}/server/apps/workflowengine:/var/www/html/apps/workflowengine" \ -v "${ENV_ROOT}/server/dist/workflowengine-workflowengine.js:/var/www/html/dist/workflowengine-workflowengine.js" \ -v "${ENV_ROOT}/server/dist/workflowengine-workflowengine.js.map:/var/www/html/dist/workflowengine-workflowengine.js.map" \ - "pondersource/dev-stock-nextcloud-sunet" + "pondersource/nextcloud-sunet" docker run --detach --network=testnet \ --name=sunet-ssp-mdb \ diff --git a/tests/surf-trashbin.sh b/tests/surf-trashbin.sh index 1f558b2c..5147d81a 100755 --- a/tests/surf-trashbin.sh +++ b/tests/surf-trashbin.sh @@ -63,7 +63,7 @@ docker run --detach --network=testnet -v "${ENV_ROOT}/temp/oc-surf-trashbin.sh:/init.sh" \ -v "${ENV_ROOT}/surf-trashbin-app:/var/www/html/apps/surf-trashbin-app" \ -v "${ENV_ROOT}/docker/tls:/tls-host" \ - pondersource/dev-stock-owncloud-surf-trashbin + pondersource/owncloud-surf-trashbin waitForPort maria1.docker 3306 waitForPort oc1.docker 443 diff --git a/tests/testing-ocm-nc-oc.sh b/tests/testing-ocm-nc-oc.sh index fed4b0f0..37de8dfb 100755 --- a/tests/testing-ocm-nc-oc.sh +++ b/tests/testing-ocm-nc-oc.sh @@ -50,7 +50,7 @@ docker run --detach --network=testnet -e PASS="relativity" \ -v "${REPO_ROOT}/temp/oc-opencloudmesh.sh:/init.sh" \ -v "${REPO_ROOT}/ocm:/var/www/html/apps/oc-opencloudmesh" \ - pondersource/dev-stock-owncloud-opencloudmesh + pondersource/owncloud-opencloudmesh echo "starting maria2.docker" docker run --detach --network=testnet \ @@ -72,7 +72,7 @@ docker run --detach --network=testnet -e USER="marie" \ -e PASS="radioactivity" \ -v "${REPO_ROOT}/temp/nc-base.sh:/init.sh" \ - pondersource/dev-stock-nextcloud + pondersource/nextcloud waitForPort maria1.docker 3306 waitForPort oc1.docker 443 diff --git a/tests/testing-ocm.sh b/tests/testing-ocm.sh index 3d39ff64..9e356a01 100755 --- a/tests/testing-ocm.sh +++ b/tests/testing-ocm.sh @@ -49,7 +49,7 @@ docker run --detach --network=testnet -e PASS="relativity" \ -v "${REPO_ROOT}/temp/oc-opencloudmesh.sh:/init.sh" \ -v "${REPO_ROOT}/ocm:/var/www/html/apps/oc-opencloudmesh" \ - pondersource/dev-stock-owncloud-opencloudmesh + pondersource/owncloud-opencloudmesh echo "starting maria2.docker" docker run --detach --network=testnet \ @@ -72,7 +72,7 @@ docker run --detach --network=testnet -e PASS="radioactivity" \ -v "${REPO_ROOT}/temp/oc-opencloudmesh.sh:/init.sh" \ -v "${REPO_ROOT}/ocm:/var/www/html/apps/oc-opencloudmesh" \ - pondersource/dev-stock-owncloud-opencloudmesh + pondersource/owncloud-opencloudmesh waitForPort maria1.docker 3306 waitForPort oc1.docker 443 diff --git a/tests/token-based-access.sh b/tests/token-based-access.sh index 1f23581e..f887b4cb 100755 --- a/tests/token-based-access.sh +++ b/tests/token-based-access.sh @@ -89,7 +89,7 @@ docker run --detach --network=testnet -v "${ENV_ROOT}/temp/oc-token-based-access.sh:/init.sh" \ -v "${ENV_ROOT}/surf-token-based-access:/var/www/html/apps/token-based-access" \ -v "${ENV_ROOT}/open-id-connect:/var/www/html/apps/openidconnect" \ - pondersource/dev-stock-owncloud-token-based-access + pondersource/owncloud-token-based-access echo "starting redis2.docker service" docker run --detach --network=testnet \ @@ -119,7 +119,7 @@ docker run --detach --network=testnet -v "${ENV_ROOT}/temp/oc-token-based-access.sh:/init.sh" \ -v "${ENV_ROOT}/surf-token-based-access:/var/www/html/apps/token-based-access" \ -v "${ENV_ROOT}/open-id-connect:/var/www/html/apps/openidconnect" \ - pondersource/dev-stock-owncloud-token-based-access + pondersource/owncloud-token-based-access waitForPort maria1.docker 3306 waitForPort oc1.docker 443 From ff0a4024e62464f0b2f2f94c8153f9ef6ae7c7f3 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 24 Dec 2024 08:43:40 +0000 Subject: [PATCH 138/184] refactor: reva script names --- docker/dockerfiles/revad.Dockerfile | 2 +- docker/scripts/reva/init.sh | 216 +++++++++++++++++++++++++++ docker/scripts/reva/terminate.sh | 86 +++++++++++ docker/scripts/seafile/entrypoint.sh | 165 ++++++++++++++++++++ scripts/reva/restart_in_conatiner.sh | 4 +- 5 files changed, 470 insertions(+), 3 deletions(-) create mode 100755 docker/scripts/reva/init.sh create mode 100755 docker/scripts/reva/terminate.sh create mode 100755 docker/scripts/seafile/entrypoint.sh diff --git a/docker/dockerfiles/revad.Dockerfile b/docker/dockerfiles/revad.Dockerfile index be00fecf..06e938d5 100644 --- a/docker/dockerfiles/revad.Dockerfile +++ b/docker/dockerfiles/revad.Dockerfile @@ -137,7 +137,7 @@ ENV PATH="${PATH}:/reva-git/cmd/revad" # Ensure these scripts have appropriate shebang lines and `chmod +x` done. # These scripts are responsible for container lifecycle management. COPY ./scripts/reva/* /usr/bin/ -RUN chmod +x /usr/bin/run.sh /usr/bin/kill.sh /usr/bin/entrypoint.sh +RUN chmod +x /usr/bin/entrypoint.sh /usr/bin/init.sh /usr/bin/terminate.sh # ---------------------------------------------------------------------------- # Entrypoint script. diff --git a/docker/scripts/reva/init.sh b/docker/scripts/reva/init.sh new file mode 100755 index 00000000..1f0a0ef5 --- /dev/null +++ b/docker/scripts/reva/init.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Reva Environment Initialization and Configuration Script +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script initializes and configures a Reva environment in a Docker container. It: +# 1. Ensures that the /reva directory exists. +# 2. Populates /reva with Reva binaries if it's empty (copied from /reva-git). +# 3. Copies and customizes Reva configuration files from /configs/revad to /etc/revad. +# 4. Sets up TLS certificates from /certificates and /certificate-authority, updating +# the operating system's CA store as needed. +# 5. Starts the Reva daemon (revad) in the background with the updated configuration. +# +# Requirements: +# - Reva source code or binaries must exist in /reva-git if /reva is empty. +# - Reva configuration files must be present in /configs/revad. +# - Optionally, TLS certificates can be placed in /certificates and/or /certificate-authority. +# - The HOST environment variable may be set to customize placeholders in the configuration. +# +# Notes: +# - If HOST is not set, it defaults to "localhost". +# - The script attempts to update the OS certificate store with any .crt files in /tls. +# - The script does not exit if copying certificate files fails (it continues silently). +# - The Reva daemon is launched in the background; logs should be checked for issues. +# +# Example: +# HOST=revaexample ./init.sh +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to missing directories, files, or commands. +# +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Halt the script on any command failure and treat pipelines robustly. +# ----------------------------------------------------------------------------------- +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Global Variables +# ----------------------------------------------------------------------------------- +REVA_DIR="/reva" # Directory where Reva binaries will reside +REVA_GIT_DIR="/reva-git" # Directory containing Reva source/binaries for copying +CONFIG_DIR="/configs/revad" # Directory containing Reva configuration files +REVA_CONFIG_DIR="/etc/revad" # Directory where config files will be copied +TLS_DIR="/tls" # Directory where certificates/keys are stored +CERTS_DIR="/certificates" # Directory containing optional certificate files +CA_DIR="/certificate-authority" # Directory containing optional CA certificate/key files +HOST="${HOST:-localhost}" # Hostname for the environment, defaults to "localhost" + +# ----------------------------------------------------------------------------------- +# Function: create_directory +# Purpose: Create a directory if it does not exist. +# Arguments: +# 1. dir (string) - The path to the directory to create. +# Returns: +# 0 on success, 1 on failure. +# ----------------------------------------------------------------------------------- +create_directory() { + local dir="$1" + + if [[ -z "$dir" ]]; then + printf "Error: No directory provided to create_directory.\n" >&2 + return 1 + fi + + mkdir -p "$dir" +} + +# ----------------------------------------------------------------------------------- +# Function: populate_reva_binaries +# Purpose: Copy Reva binaries from /reva-git to /reva if /reva is empty. +# Behavior: +# - If /reva does not exist, print an error and return 1. +# - If /reva is empty, copy the 'cmd' directory from /reva-git to /reva. +# - If /reva is not empty, list the contents and do nothing. +# Returns: +# 0 on success, 1 on failure. +# ----------------------------------------------------------------------------------- +populate_reva_binaries() { + if [[ ! -d "$REVA_DIR" ]]; then + printf "Error: %s does not exist.\n" "$REVA_DIR" >&2 + return 1 + fi + + # Check if /reva is empty + if [[ -z "$(find "$REVA_DIR" -mindepth 1 -print -quit 2>/dev/null)" ]]; then + printf "/reva is an empty directory, populating it with Reva binaries...\n" + # Copy 'cmd' from /reva-git into /reva + if ! cp -ar "$REVA_GIT_DIR/cmd" "$REVA_DIR"; then + printf "Error: Failed to copy binaries to /reva.\n" >&2 + return 1 + fi + else + # If not empty, list the contents + ls -lsa "$REVA_DIR" + printf "/reva contains files, doing nothing.\n" + fi +} + +# ----------------------------------------------------------------------------------- +# Function: prepare_configuration +# Purpose: Copy Reva configuration files to /etc/revad and replace placeholders. +# Behavior: +# - If /configs/revad is missing, print an error and return 1. +# - Remove any existing /etc/revad directory. +# - Copy /configs/revad to /etc/revad. +# - Replace placeholder hostnames in .toml files with $HOST.docker or derived host. +# Returns: +# 0 on success, 1 on failure. +# ----------------------------------------------------------------------------------- +prepare_configuration() { + if [[ ! -d "$CONFIG_DIR" ]]; then + printf "Error: Configuration directory %s not found.\n" "$CONFIG_DIR" >&2 + return 1 + fi + + # Remove existing config directory + rm -rf "$REVA_CONFIG_DIR" + + # Copy config directory + if ! cp -r "$CONFIG_DIR" "$REVA_CONFIG_DIR"; then + printf "Error: Failed to copy configuration to %s.\n" "$REVA_CONFIG_DIR" >&2 + return 1 + fi + + # Replace placeholders in .toml files + sed -i "s/your.revad.org/${HOST}.docker/g" "$REVA_CONFIG_DIR"/*.toml || true + sed -i "s/localhost/${HOST}.docker/g" "$REVA_CONFIG_DIR"/*.toml || true + sed -i "s/your.efss.org/${HOST//reva/}.docker/g" "$REVA_CONFIG_DIR"/*.toml || true + sed -i "s/your.nginx.org/${HOST//reva/}.docker/g" "$REVA_CONFIG_DIR"/*.toml || true +} + +# ----------------------------------------------------------------------------------- +# Function: prepare_tls_certificates +# Purpose: Create /tls, copy certificate files from /certificates and /certificate-authority, +# update the OS certificate store, and create symbolic links for server.crt/key. +# Behavior: +# - If /certificates or /certificate-authority exist, copy .crt and .key files to /tls. +# - Update the OS CA store with any .crt files found in /tls. +# - Link ${HOST}.crt to server.crt and ${HOST}.key to server.key in /tls. +# Returns: +# 0 on success, does not fail if copying certificate files fails (silent ignore). +# ----------------------------------------------------------------------------------- +prepare_tls_certificates() { + create_directory "$TLS_DIR" + + # Copy certificates from /certificates + if [[ -d "$CERTS_DIR" ]]; then + cp -f "$CERTS_DIR"/*.crt "$TLS_DIR/" 2>/dev/null || true + cp -f "$CERTS_DIR"/*.key "$TLS_DIR/" 2>/dev/null || true + fi + + # Copy certificates from /certificate-authority + if [[ -d "$CA_DIR" ]]; then + cp -f "$CA_DIR"/*.crt "$TLS_DIR/" 2>/dev/null || true + cp -f "$CA_DIR"/*.key "$TLS_DIR/" 2>/dev/null || true + fi + + # Update OS CA store (ignore errors) + cp -f "$TLS_DIR"/*.crt /usr/local/share/ca-certificates/ 2>/dev/null || true + update-ca-certificates || true + + # Create symlinks for Reva's server certificates + ln -sf "$TLS_DIR/${HOST}.crt" "$TLS_DIR/server.crt" + ln -sf "$TLS_DIR/${HOST}.key" "$TLS_DIR/server.key" +} + +# ----------------------------------------------------------------------------------- +# Function: start_reva_daemon +# Purpose: Launch the Reva daemon (revad) with the dev-dir set to /etc/revad. +# Behavior: +# - Checks for revad in PATH. +# - Starts revad in the background. +# Returns: +# 0 on success, 1 if revad is not found. +# ----------------------------------------------------------------------------------- +start_reva_daemon() { + if ! command -v revad &>/dev/null; then + printf "Error: Reva daemon (revad) not found in PATH.\n" >&2 + return 1 + fi + + printf "Starting Reva daemon...\n" + # Start revad in the background + revad --dev-dir "$REVA_CONFIG_DIR" & +} + +# ----------------------------------------------------------------------------------- +# Main function to coordinate the script flow. +# ----------------------------------------------------------------------------------- +main() { + # Ensure /reva directory exists + create_directory "$REVA_DIR" + + # Populate /reva if empty + populate_reva_binaries + + # Prepare Reva configuration files + prepare_configuration + + # Handle TLS certificates + prepare_tls_certificates + + # Start the Reva daemon + start_reva_daemon +} + +# ----------------------------------------------------------------------------------- +# Execute the main function +# ----------------------------------------------------------------------------------- +main diff --git a/docker/scripts/reva/terminate.sh b/docker/scripts/reva/terminate.sh new file mode 100755 index 00000000..10e72a03 --- /dev/null +++ b/docker/scripts/reva/terminate.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Reva Daemon Termination Script +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# Description: +# This script searches for any running Reva daemon (revad) processes and terminates them. +# It ensures a clean shutdown in development or testing environments where revad +# might still be running in the background. +# +# Requirements: +# - pgrep and kill must be installed and available in PATH. +# - The script assumes only one revad process is actively running or that the last started +# revad process is the one needing termination. +# +# Behavior: +# - Finds the PID of the most recently started revad process using `pgrep -f "revad" | tail -n 1`. +# - If no PID is found, it prints a message and exits successfully. +# - If a PID is found, it sends a SIGKILL (kill -9) to force-stop the process. +# - Logs the action taken to stdout. +# +# Notes: +# - Using `kill -9` is forceful; consider using a gentler signal (e.g., SIGTERM) in production. +# - This script is primarily intended for development/testing cleanup. +# +# Exit Codes: +# 0 - Success or no revad processes found. +# 1 - Failure to terminate the found revad process. +# +# Example: +# ./terminate.sh +# +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Exit on Errors and Treat Unset Variables as Errors +# ----------------------------------------------------------------------------------- +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Function: terminate_revad +# Purpose: Safely terminate any running revad process. +# Process: +# 1. Searches for revad processes using `pgrep -f "revad"`. +# 2. Picks the most recently started revad process (tail -n 1). +# 3. Sends a SIGKILL (kill -9) to the process. +# Returns: +# 0 if no process was found or termination was successful, +# 1 if it failed to terminate the process. +# ----------------------------------------------------------------------------------- +terminate_revad() { + # Find the PID of the most recently started revad process + local revad_pid + + revad_pid=$(pgrep -f "revad" | tail -n 1 || true) + + # Check if a PID was found + if [[ -z "$revad_pid" ]]; then + printf "No running revad process found.\n" + return 0 + fi + + # Safely terminate the revad process using kill -9 + printf "Terminating revad process with PID %s...\n" "$revad_pid" + if ! kill -9 "$revad_pid" 2>/dev/null; then + printf "Error: Failed to terminate revad process with PID %s.\n" "$revad_pid" >&2 + return 1 + fi + + printf "Successfully terminated revad process with PID %s.\n" "$revad_pid" + return 0 +} + +# ----------------------------------------------------------------------------------- +# Main function to coordinate script execution +# ----------------------------------------------------------------------------------- +main() { + terminate_revad +} + +# ----------------------------------------------------------------------------------- +# Execute the main function +# ----------------------------------------------------------------------------------- +main diff --git a/docker/scripts/seafile/entrypoint.sh b/docker/scripts/seafile/entrypoint.sh new file mode 100755 index 00000000..101a7f99 --- /dev/null +++ b/docker/scripts/seafile/entrypoint.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Seafile Seahub Settings Configuration Script +# ----------------------------------------------------------------------------------- +# Description: +# This script updates the Seafile Seahub settings to: +# 1. Enable and configure Open Cloud Mesh (OCM) integration with a unique provider UUID. +# 2. Update Memcached settings based on environment variables or defaults. +# +# Requirements: +# - Seahub settings file must exist at the specified SEAHUB_SETTINGS path. +# - The user running this script must have write permissions to the Seahub settings file. +# +# Environment Variables: +# SEAFILE_MEMCACHE_HOST (optional): Hostname for Memcached (default: "memcached") +# SEAFILE_MEMCACHE_PORT (optional): Port for Memcached (default: "11211") +# +# Arguments: +# 1 (optional): Remote server name for OCM (default: "seafile") +# +# Notes: +# - A unique UUID is generated from /proc/sys/kernel/random/uuid for OCM. +# - Modifications are appended to the Seahub settings file. Existing settings are not removed. +# +# Example: +# ./entrypoint.sh "myserver" +# +# Exit Codes: +# 0 - Success +# 1 - Failure due to missing files, permissions, or command errors. +# +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Exit on Errors and Ensure Robust Pipeline Handling +# ----------------------------------------------------------------------------------- +set -e +set -o pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Environment Variables +# ----------------------------------------------------------------------------------- +SEAHUB_SETTINGS="/opt/seafile/conf/seahub_settings.py" # Path to Seahub settings file + +# Default values for Memcached host and port +SEAFILE_MEMCACHE_HOST="${SEAFILE_MEMCACHE_HOST:-memcached}" +SEAFILE_MEMCACHE_PORT="${SEAFILE_MEMCACHE_PORT:-11211}" + +# Default remote server name for OCM (can be overridden by script argument) +DEFAULT_REMOTE_SERVER="seafile" + +# ----------------------------------------------------------------------------------- +# Function: generate_uuid +# Purpose: Generate a UUID using the kernel's random generator. +# Returns: UUID as a string. +# On Error: Prints an error and exits with code 1 if the UUID file is not found. +# ----------------------------------------------------------------------------------- +generate_uuid() { + local uuid_file="/proc/sys/kernel/random/uuid" + if [[ -f "${uuid_file}" ]]; then + cat "${uuid_file}" + else + echo "Error: UUID generator file not found at ${uuid_file}." >&2 + exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Function: append_ocm_configuration +# Purpose: Append OCM configuration to the Seahub settings file. +# Arguments: +# 1. settings_file: Path to the Seahub settings file +# 2. uuid: Unique provider ID for OCM +# 3. remote_server: Remote server name for OCM +# On Error: Prints an error and exits if the file is not writable. +# ----------------------------------------------------------------------------------- +append_ocm_configuration() { + local settings_file="$1" + local uuid="$2" + local remote_server="$3" + + # Verify write permission for the settings file + if [[ ! -w "${settings_file}" ]]; then + echo "Error: Cannot write to ${settings_file}. Check file permissions." >&2 + exit 1 + fi + + printf "Appending OCM configuration to %s...\n" "${settings_file}" + + cat >> "${settings_file}" <&2 + exit 1 + fi + + printf "Memcached configuration updated successfully to %s:%s\n" "${memcache_host}" "${memcache_port}" +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- +main() { + # Verify Seahub settings file exists + if [[ ! -f "${SEAHUB_SETTINGS}" ]]; then + echo "Error: Seahub settings file not found at ${SEAHUB_SETTINGS}" >&2 + exit 1 + fi + + # Generate a UUID for the OCM provider + printf "Generating UUID for OCM provider...\n" + uuid=$(generate_uuid) + printf "Generated UUID: %s\n" "${uuid}" + + # Parse input argument for remote server name + # If no argument provided, use DEFAULT_REMOTE_SERVER + remote_server=${1:-"${DEFAULT_REMOTE_SERVER}"} + printf "Using remote server: %s\n" "${remote_server}" + + # Append OCM configuration to Seahub settings + append_ocm_configuration "${SEAHUB_SETTINGS}" "${uuid}" "${remote_server}" + + # Update memcached configuration + update_memcached_configuration "${SEAHUB_SETTINGS}" "${SEAFILE_MEMCACHE_HOST}" "${SEAFILE_MEMCACHE_PORT}" + + printf "Seafile Seahub configuration completed successfully.\n" +} + +# Execute the main function +main "$@" diff --git a/scripts/reva/restart_in_conatiner.sh b/scripts/reva/restart_in_conatiner.sh index c771aba8..efafdc8c 100755 --- a/scripts/reva/restart_in_conatiner.sh +++ b/scripts/reva/restart_in_conatiner.sh @@ -50,7 +50,7 @@ restart_reva_in_container() { printf "Restarting 'reva' process in container: %s\n" "$container_name" # Kill 'reva' process inside the container - if ! docker exec "$container_name" bash -c "reva-kill.sh"; then + if ! docker exec "$container_name" bash -c "/terminate.sh"; then print_error "Failed to kill 'reva' process in container: $container_name" success=false else @@ -58,7 +58,7 @@ restart_reva_in_container() { fi # Start 'reva' process inside the container - if ! docker exec "$container_name" bash -c "reva-run.sh"; then + if ! docker exec "$container_name" bash -c "/init.sh"; then print_error "Failed to start 'reva' process in container: $container_name" success=false else From ee7db232cca8ec7115209e2cfc69e7a5059a52ff Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 22 Jan 2025 14:50:56 +0330 Subject: [PATCH 139/184] refactor: streamline seafile-seafile.sh script for improved readability and functionality --- .../share-with/seafile-seafile.sh | 379 +++++++----------- 1 file changed, 136 insertions(+), 243 deletions(-) diff --git a/dev/ocm-test-suite/share-with/seafile-seafile.sh b/dev/ocm-test-suite/share-with/seafile-seafile.sh index e6398e54..1d9b29a1 100755 --- a/dev/ocm-test-suite/share-with/seafile-seafile.sh +++ b/dev/ocm-test-suite/share-with/seafile-seafile.sh @@ -1,252 +1,145 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# seafile version: -# - 8.0.8 -# - 9.0.10 -# - 10.0.1 -# - 11.0.5 -EFSS_PLATFORM_1_VERSION=${1:-"11.0.5"} - -# seafile version: -# - 8.0.8 -# - 9.0.10 -# - 10.0.1 -# - 11.0.5 -EFSS_PLATFORM_2_VERSION=${2:-"11.0.5"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi +# ----------------------------------------------------------------------------------- +# Script to Test Seafile-to-Seafile share-with flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of Seafile-to-Seafile sharing using +# Cypress and Docker containers. It supports both development and CI environments. + +# Usage: +# ./seafile-seafile.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first Seafile instance (default: "v11.0.5"). +# EFSS_PLATFORM_2_VERSION : Version of the second Seafile instance (default: "v11.0.5"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Example: +# ./seafile-seafile.sh v11.0.5 v11.0.5 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v11.0.5" +DEFAULT_EFSS_2_VERSION="v11.0.5" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi -function waitForCollabora() { - x=$(docker logs collabora.docker | grep -c "Ready") - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo "Waiting for Collabora to be ready, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker logs collabora.docker | grep -c "Ready") - done - redirect_to_null_cmd echo "Collabora is ready" + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi } -function createEfssSeafile() { - local platform="${1}" - local number="${2}" - local user_email="${3}" - local password="${4}" - local remote_ocm_server="${5}" - local tag="${6-latest}" - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="memcache${platform}${number}.docker" \ - memcached:1.6.18 \ - memcached -m 256 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - -e TIME_ZONE="Etc/UTC" \ - -e DB_HOST="maria${platform}${number}.docker" \ - -e DB_ROOT_PASSWD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" \ - -e SEAFILE_ADMIN_EMAIL="${user_email}" \ - -e SEAFILE_ADMIN_PASSWORD="${password}" \ - -e SEAFILE_SERVER_LETSENCRYPT=false \ - -e FORCE_HTTPS_IN_CONF=false \ - -e SEAFILE_SERVER_HOSTNAME="${platform}${number}.docker" \ - -e SEAFILE_MEMCACHE_HOST="memcache${platform}${number}.docker" \ - -e SEAFILE_MEMCACHE_PORT=11211 \ - -v "${ENV_ROOT}/temp/sea-init.sh:/init.sh" \ - -v "${ENV_ROOT}/temp/seafile-data/${platform}${number}:/shared" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.crt:/shared/ssl/${platform}${number}.docker.crt" \ - -v "${ENV_ROOT}/docker/tls/certificates/${platform}${number}.key:/shared/ssl/${platform}${number}.docker.key" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - "seafileltd/seafile-mc:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - - # seafile needs time to bootstrap itself. - sleep 5 - - # run init script inside seafile. - redirect_to_null_cmd docker exec -e remote_ocm_server="${remote_ocm_server}" "${platform}${number}.docker" bash -c "/init.sh ${remote_ocm_server}" - - # restart seafile to apply our changes. - sleep 2 - redirect_to_null_cmd docker restart "${platform}${number}.docker" - sleep 2 - - redirect_to_null_cmd echo "" +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE +# +# Arguments: +# All command line arguments are passed to parse_arguments. +# +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + # # id # username # password # image # tag # remote_ocm_server + create_seafile 1 "jonathan@seafile.com" "xu" seafileltd/seafile-mc "${EFSS_PLATFORM_1_VERSION}" "seafile2" + create_seafile 2 "giuseppe@cern.ch" "lopresti" seafileltd/seafile-mc "${EFSS_PLATFORM_2_VERSION}" "seafile1" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://seafile1.docker (username: jonathan@seafile.com, password: xu)" \ + "https://seafile2.docker (username: giuseppe@cern.ch, password: lopresti)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi } -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp" - -# copy init files. -cp -f "${ENV_ROOT}/docker/scripts/init/seafile.sh" "${ENV_ROOT}/temp/sea-init.sh" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - - -############### -### Seafile ### -############### - -# Seafiles. -createEfssSeafile seafile 1 jonathan@seafile.com xu seafile2 "${EFSS_PLATFORM_1_VERSION}" -createEfssSeafile seafile 2 giuseppe@cern.ch lopresti seafile1 "${EFSS_PLATFORM_2_VERSION}" - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://seafile1.docker -> username: jonathan@seafile.com password: xu" - echo "https://seafile2.docker -> username: giuseppe@cern.ch password: lopresti" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/share-with/seafile-${P1_VER}-to-seafile-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From 823fbb8170daa699cb9f14a96595e9a79422b0cf Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Thu, 23 Jan 2025 14:07:03 +0330 Subject: [PATCH 140/184] modify: file name to have v before numbers --- ...le-11-to-seafile-11.cy.js => seafile-v11-to-seafile-v11.cy.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cypress/ocm-test-suite/cypress/e2e/share-with/{seafile-11-to-seafile-11.cy.js => seafile-v11-to-seafile-v11.cy.js} (100%) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-seafile-11.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/share-with/seafile-11-to-seafile-11.cy.js rename to cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js From b0c3ee650e7c66b9e3eaca3863e588a58664490c Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Thu, 23 Jan 2025 14:09:45 +0330 Subject: [PATCH 141/184] [no ci] refactor: enhance Cypress tests for Seafile v11 by modularizing share functionality and improving readability --- .../seafile-v11-to-seafile-v11.cy.js | 96 ++++----------- .../cypress/e2e/utils/seafile-v11.js | 113 ++++++++++++++++-- 2 files changed, 127 insertions(+), 82 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js index 6cc192fb..b3ecca27 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js @@ -1,16 +1,22 @@ /** * @fileoverview - * Cypress test suite for testing native federated sharing functionality in Seafile. + * Cypress test suite for testing native federated sharing functionality in Seafile v11. * * @author Mohammad Mahdi Baghbani Pourvahid */ import { + loginSeafile, dismissModalIfPresentV11, + openShareDialog, + openFederatedSharingTab, + selectRemoteServer, + shareWithRemoteUser, + navigateToReceivedShares, + verifyReceivedShare } from '../utils/seafile-v11'; -describe('Native federated sharing functionality for Seafile', () => { - +describe('Native federated sharing functionality for Seafile v11', () => { // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('SEAFILE1_URL') || 'http://seafile1.docker'; const recipientUrl = Cypress.env('SEAFILE2_URL') || 'http://seafile2.docker'; @@ -26,64 +32,22 @@ describe('Native federated sharing functionality for Seafile', () => { */ it('should successfully send a federated share of a file from Seafile 1 to Seafile 2', () => { // Step 1: Log in to Seafile 1 - cy.loginSeafile(senderUrl, senderUsername, senderPassword); + loginSeafile(senderUrl, senderUsername, senderPassword); - // Step 2: Dismiss any modals if present (e.g., welcome or info dialogs) + // Step 2: Dismiss any modals if present dismissModalIfPresentV11(); - // Step 3: Locate the file to share and open the share menu. - // Adjust selectors as per actual DOM structure: - // - eq(0) targets the first file row. - // - eq(3) selects the 4th column cell in that row (where "Share" button is assumed). - cy.get('#wrapper .main-panel .reach-router .main-panel-center .cur-view-container .cur-view-content') - .find('table tbody') - .eq(0) // First file - .find('tr td') - .eq(3) // Column containing the Share button - .trigger('mouseover') // Hover to reveal the share button if hidden - .find('[title="Share"]') // Locate the share button by title attribute - .should('be.visible') - .click(); - - // Step 4: Select the federated sharing option - // eq(4) is the 5th item in the share dialog side menu, assumed to be "Federated Sharing" - cy.get('.share-dialog-side ul li') - .eq(4) - .should('be.visible') - .click(); - - // Step 5: Select the Seafile server from a dropdown - // Interact with the server selection dropdown - cy.get('#share-to-other-server-panel table tbody') - .eq(0) - // The dropdown trigger is assumed to be an SVG icon - .find('tr td svg') - .eq(0) - .should('be.visible') - .click(); + // Step 3: Open share dialog for the first file + openShareDialog(); - // Select a server from the resulting dialog - // Using a regex to match a server name that starts with 'seafile' followed by word characters - cy.get('[role="dialog"]') - .contains(/^seafile\w+/) - .should('be.visible') - .click(); + // Step 4: Open federated sharing tab + openFederatedSharingTab(); - // Step 6: Enter the recipient's email and submit the share - // Within the panel, type the recipient email and click "Submit" - cy.get('#share-to-other-server-panel table tbody') - .eq(0) - .within(() => { - cy.get('input.form-control') - .should('be.visible') - .type(recipientUsername); + // Step 5: Select the remote Seafile server + selectRemoteServer('seafile2'); - cy.contains('Submit') - .should('be.visible') - .click(); - }); - - // Optional: Add assertions to verify a success message or notification, if available. + // Step 6: Share with remote user + shareWithRemoteUser(recipientUsername); }); /** @@ -91,25 +55,15 @@ describe('Native federated sharing functionality for Seafile', () => { */ it('should successfully receive and display a federated share of a file on Seafile 2', () => { // Step 1: Log in to Seafile 2 - cy.loginSeafile(recipientUrl, recipientUsername, recipientPassword); + loginSeafile(recipientUrl, recipientUsername, recipientPassword); - // Step 2: Dismiss any modals if present (e.g., welcome or info dialogs) + // Step 2: Dismiss any modals if present dismissModalIfPresentV11(); - // Step 3: Navigate to the "Received Shares" section - // eq(5) selects the 6th menu item in the sidebar assumed to be "Received Shares" - cy.get('#wrapper .side-panel .side-panel-center .side-nav .side-nav-con ul li') - .eq(5) - .should('be.visible') - .click(); - - // Step 4: Validate that the shared file is visible - // Check that the received shares table is visible and that it has at least one row - cy.get('.received-shares-table') - .should('be.visible') - .find('tr') - .should('have.length.greaterThan', 0); + // Step 3: Navigate to received shares section + navigateToReceivedShares(); - // Optional: Further assertions could be made to verify that the expected file name appears. + // Step 4: Verify the received share is visible + verifyReceivedShare(); }); }); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js index c43ea8f6..ef79b3ad 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js @@ -7,17 +7,108 @@ * @author Mohammad Mahdi Baghbani Pourvahid */ -// Utility function to dismiss a modal if it appears +/** + * Login to Seafile instance. + * @param {string} url - The URL of the Seafile instance + * @param {string} username - The username to log in with + * @param {string} password - The password to log in with + */ +export function loginSeafile(url, username, password) { + cy.visit(url); + cy.get('input[name="login"]').type(username); + cy.get('input[name="password"]').type(password); + cy.get('button[type="submit"]').click(); +} + +/** + * Dismiss any modal dialogs that might be present (e.g., welcome or info dialogs). + */ export function dismissModalIfPresentV11() { - // No waiting; check immediately - cy.get('[role="dialog"]', { timeout: 5000 }) - .then((modals) => { - if (modals.length > 0) { - // If modal exists, close it - cy.wrap(modals) - .find('.modal-dialog .modal-content .modal-body button') - .should('be.visible') - .click(); - } + // Add any modal dismissal logic specific to v11 here + cy.get('body').then($body => { + if ($body.find('.modal-dialog').length > 0) { + cy.get('.modal-dialog .close').click(); + } + }); +} + +/** + * Open the share dialog for the first file in the list. + */ +export function openShareDialog() { + cy.get('#wrapper .main-panel .reach-router .main-panel-center .cur-view-container .cur-view-content') + .find('table tbody') + .eq(0) // First file + .find('tr td') + .eq(3) // Column containing the Share button + .trigger('mouseover') // Hover to reveal the share button if hidden + .find('[title="Share"]') // Locate the share button by title attribute + .should('be.visible') + .click(); +} + +/** + * Open the federated sharing tab in the share dialog. + */ +export function openFederatedSharingTab() { + cy.get('.share-dialog-side ul li') + .eq(4) // 5th item in the share dialog side menu is "Federated Sharing" + .should('be.visible') + .click(); +} + +/** + * Select a remote server from the dropdown. + * @param {string} serverName - The name of the remote server to select + */ +export function selectRemoteServer(serverName) { + cy.get('#share-to-other-server-panel table tbody') + .eq(0) + .find('tr td svg') // The dropdown trigger + .eq(0) + .should('be.visible') + .click(); + + cy.get('[role="dialog"]') + .contains(new RegExp(`^${serverName}\\w*`)) + .should('be.visible') + .click(); +} + +/** + * Share a file with a remote user. + * @param {string} remoteUsername - The username of the remote user to share with + */ +export function shareWithRemoteUser(remoteUsername) { + cy.get('#share-to-other-server-panel table tbody') + .eq(0) + .within(() => { + cy.get('input.form-control') + .should('be.visible') + .type(remoteUsername); + + cy.contains('Submit') + .should('be.visible') + .click(); }); } + +/** + * Navigate to the received shares section. + */ +export function navigateToReceivedShares() { + cy.get('#wrapper .side-panel .side-panel-center .side-nav .side-nav-con ul li') + .eq(5) // 6th menu item is "Received Shares" + .should('be.visible') + .click(); +} + +/** + * Verify that a received share is visible in the list. + */ +export function verifyReceivedShare() { + cy.get('.received-shares-table') + .should('be.visible') + .find('tr') + .should('have.length.greaterThan', 0); +} From 563f07b0a421e18e1967549415442f99175a8187 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Thu, 23 Jan 2025 15:23:03 +0330 Subject: [PATCH 142/184] [no ci] refactor: replace custom login function with Cypress command in Seafile v11 tests --- .../e2e/share-with/seafile-v11-to-seafile-v11.cy.js | 5 ++--- .../ocm-test-suite/cypress/e2e/utils/seafile-v11.js | 13 ------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js index b3ecca27..0e32782e 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js @@ -6,7 +6,6 @@ */ import { - loginSeafile, dismissModalIfPresentV11, openShareDialog, openFederatedSharingTab, @@ -32,7 +31,7 @@ describe('Native federated sharing functionality for Seafile v11', () => { */ it('should successfully send a federated share of a file from Seafile 1 to Seafile 2', () => { // Step 1: Log in to Seafile 1 - loginSeafile(senderUrl, senderUsername, senderPassword); + cy.loginSeafile(senderUrl, senderUsername, senderPassword); // Step 2: Dismiss any modals if present dismissModalIfPresentV11(); @@ -55,7 +54,7 @@ describe('Native federated sharing functionality for Seafile v11', () => { */ it('should successfully receive and display a federated share of a file on Seafile 2', () => { // Step 1: Log in to Seafile 2 - loginSeafile(recipientUrl, recipientUsername, recipientPassword); + cy.loginSeafile(recipientUrl, recipientUsername, recipientPassword); // Step 2: Dismiss any modals if present dismissModalIfPresentV11(); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js index ef79b3ad..cae94858 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js @@ -7,19 +7,6 @@ * @author Mohammad Mahdi Baghbani Pourvahid */ -/** - * Login to Seafile instance. - * @param {string} url - The URL of the Seafile instance - * @param {string} username - The username to log in with - * @param {string} password - The password to log in with - */ -export function loginSeafile(url, username, password) { - cy.visit(url); - cy.get('input[name="login"]').type(username); - cy.get('input[name="password"]').type(password); - cy.get('button[type="submit"]').click(); -} - /** * Dismiss any modal dialogs that might be present (e.g., welcome or info dialogs). */ From 4cae2f5463f7c5ebe6efb27276519f8f1d6e9e27 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Sat, 25 Jan 2025 11:56:33 +0330 Subject: [PATCH 143/184] [no ci] fix: verifyReceivedShare function in Seafile v11 tests --- .../e2e/share-with/seafile-v11-to-seafile-v11.cy.js | 4 +--- .../ocm-test-suite/cypress/e2e/utils/seafile-v11.js | 11 +++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js index 0e32782e..71c8838b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js @@ -23,8 +23,6 @@ describe('Native federated sharing functionality for Seafile v11', () => { const senderPassword = Cypress.env('SEAFILE1_PASSWORD') || 'xu'; const recipientUsername = Cypress.env('SEAFILE2_USERNAME') || 'giuseppe@cern.ch'; const recipientPassword = Cypress.env('SEAFILE2_PASSWORD') || 'lopresti'; - const originalFileName = 'welcome.txt'; - const sharedFileName = 'share-with-nc1-to-nc2.txt'; /** * Test Case: Sending a federated share from Seafile 1 to Seafile 2. @@ -63,6 +61,6 @@ describe('Native federated sharing functionality for Seafile v11', () => { navigateToReceivedShares(); // Step 4: Verify the received share is visible - verifyReceivedShare(); + verifyReceivedShare(senderUsername); }); }); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js index cae94858..051f5188 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js @@ -93,9 +93,12 @@ export function navigateToReceivedShares() { /** * Verify that a received share is visible in the list. */ -export function verifyReceivedShare() { - cy.get('.received-shares-table') +export function verifyReceivedShare(remoteUsername) { + cy.get('#wrapper .main-panel .reach-router .main-panel-center .cur-view-container .cur-view-content') + .find('table tbody') + .eq(0) // First file + .find('tr td') + .eq(3) // Column containing the Share sender .should('be.visible') - .find('tr') - .should('have.length.greaterThan', 0); + .should('contain', remoteUsername); } From 88665adc2843857b375e92f18dbf98bfb28b7c49 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Sat, 25 Jan 2025 12:23:44 +0330 Subject: [PATCH 144/184] [no ci] feat: Improve modal dismissal and share verification in Seafile v11 tests --- .../seafile-v11-to-seafile-v11.cy.js | 2 +- .../cypress/e2e/utils/seafile-v11.js | 42 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js index 71c8838b..f3123ae0 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/seafile-v11-to-seafile-v11.cy.js @@ -61,6 +61,6 @@ describe('Native federated sharing functionality for Seafile v11', () => { navigateToReceivedShares(); // Step 4: Verify the received share is visible - verifyReceivedShare(senderUsername); + verifyReceivedShare(senderUsername, senderUrl); }); }); diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js index 051f5188..83cccdce 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/seafile-v11.js @@ -9,14 +9,27 @@ /** * Dismiss any modal dialogs that might be present (e.g., welcome or info dialogs). + * Waits up to 3.5 seconds for a modal to appear before attempting to dismiss it. */ export function dismissModalIfPresentV11() { - // Add any modal dismissal logic specific to v11 here - cy.get('body').then($body => { - if ($body.find('.modal-dialog').length > 0) { - cy.get('.modal-dialog .close').click(); - } - }); + // Wait for up to 1 seconds for any modal to appear + cy.get('body') + .wait(1000) // Wait for potential delayed modals + .then($body => { + // Check for modal every 250ms for up to 2.5 seconds + const checkModal = (attempts = 0) => { + if (attempts >= 10) return; // Max 10 attempts (2.5 seconds) + + if ($body.find('.modal-dialog').length > 0) { + cy.get('.modal-dialog .close').click(); + } else { + // If no modal found, wait 500ms and check again + cy.wait(250).then(() => checkModal(attempts + 1)); + } + }; + + checkModal(); + }); } /** @@ -93,12 +106,19 @@ export function navigateToReceivedShares() { /** * Verify that a received share is visible in the list. */ -export function verifyReceivedShare(remoteUsername) { +export function verifyReceivedShare(remoteUsername, remoteServer) { cy.get('#wrapper .main-panel .reach-router .main-panel-center .cur-view-container .cur-view-content') .find('table tbody') .eq(0) // First file - .find('tr td') - .eq(3) // Column containing the Share sender - .should('be.visible') - .should('contain', remoteUsername); + .within(() => { + cy.get('tr td') + .eq(2) // Column containing the Share sender + .should('be.visible') + .should('contain', remoteUsername); + + cy.get('tr td') + .eq(3) // Column containing the Share receiver + .should('be.visible') + .should('contain', remoteServer); + }); } From 364bcb8416ee7adc2348fbd4df7670c01e8bc31c Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Sat, 25 Jan 2025 12:44:58 +0330 Subject: [PATCH 145/184] fix: Workflow names for NC v28 and OC v10 test configurations --- .github/workflows/share-with-os-v1-nc-v28.yml | 2 +- .github/workflows/share-with-os-v1-oc-10.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/share-with-os-v1-nc-v28.yml b/.github/workflows/share-with-os-v1-nc-v28.yml index f91a0e26..c75f5700 100644 --- a/.github/workflows/share-with-os-v1-nc-v28.yml +++ b/.github/workflows/share-with-os-v1-nc-v28.yml @@ -1,4 +1,4 @@ -name: OCM Test Share With OcmStub v1.0.0 to NC v27.1.11 +name: OCM Test Share With OcmStub v1.0.0 to NC v28.0.14 # Controls when the action will run. on: diff --git a/.github/workflows/share-with-os-v1-oc-10.yml b/.github/workflows/share-with-os-v1-oc-10.yml index 33eb35b3..68060c85 100644 --- a/.github/workflows/share-with-os-v1-oc-10.yml +++ b/.github/workflows/share-with-os-v1-oc-10.yml @@ -1,4 +1,4 @@ -name: OCM Test Share With OcmStub v1.0.0 to NC v27.1.11 +name: OCM Test Share With OcmStub v1.0.0 to OC v10.15.0 # Controls when the action will run. on: From 6bbec79d1ebcd91d909273034a1219f901b3828b Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 10:37:24 +0330 Subject: [PATCH 146/184] add: initial nextcloud app dockerfile --- docker/dockerfiles/nextcloud-app.Dockerfile | 61 +++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docker/dockerfiles/nextcloud-app.Dockerfile diff --git a/docker/dockerfiles/nextcloud-app.Dockerfile b/docker/dockerfiles/nextcloud-app.Dockerfile new file mode 100644 index 00000000..6a4a9ab9 --- /dev/null +++ b/docker/dockerfiles/nextcloud-app.Dockerfile @@ -0,0 +1,61 @@ +ARG NEXTCLOUD_VERSION=latest +FROM pondersource/nextcloud:${NEXTCLOUD_VERSION} + +# App installation arguments +ARG APP_NAME +ARG APP_REPO +ARG APP_BRANCH=main +ARG APP_BUILD_CMD="" +ARG APP_SOURCE_DIR="/ponder/apps" +ARG INIT_SCRIPT="" + +# keys for oci taken from: +# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys +LABEL org.opencontainers.image.licenses=MIT +LABEL org.opencontainers.image.title="PonderSource Nextcloud with ${APP_NAME}" +LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" +LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" +LABEL org.opencontainers.image.description="Nextcloud image with ${APP_NAME} pre-installed" + +USER root + +RUN set -ex; \ + \ + apt-get update; \ + apt-get install --no-install-recommends --assume-yes \ + git + +RUN mkdir -p ${APP_SOURCE_DIR}; \ + chown -R www-data:root ${APP_SOURCE_DIR}; \ + chmod -R g=u ${APP_SOURCE_DIR} + +USER www-data + +# Install the app +RUN set -ex; \ + if [ -z "${APP_NAME}" ] || [ -z "${APP_REPO}" ]; then \ + echo "Error: APP_NAME and APP_REPO must be provided"; \ + exit 1; \ + fi; \ + # Clone the app repository + git clone \ + --depth 1 \ + --branch ${APP_BRANCH} \ + ${APP_REPO} \ + ${APP_SOURCE_DIR}/${APP_NAME}; \ + # Update to latest commit + cd ${APP_SOURCE_DIR}/${APP_NAME} && git pull; \ + # Build if build command is provided + if [ -n "${APP_BUILD_CMD}" ]; then \ + ${APP_BUILD_CMD}; \ + fi + +USER root + +# After cloning, `git` is no longer needed at runtime, so remove it to reduce image size. +RUN apt-get purge -y git && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* + + +# Copy init script if provided +COPY ${INIT_SCRIPT} "/docker-entrypoint-hooks.d/before-starting/${APP_NAME}.sh" +RUN chmod +x /docker-entrypoint-hooks.d/before-starting/${APP_NAME}.sh From 131165d545c5c48d88136abafad4da1f87112eef Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 10:40:42 +0330 Subject: [PATCH 147/184] update: build image for sciencemesh --- docker/build/all.sh | 141 ++++++++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 56 deletions(-) diff --git a/docker/build/all.sh b/docker/build/all.sh index adc4c464..9a85b572 100755 --- a/docker/build/all.sh +++ b/docker/build/all.sh @@ -111,7 +111,7 @@ command_exists() { } # ----------------------------------------------------------------------------------- -# Docker Build Function +# Docker Build Functions # ----------------------------------------------------------------------------------- # A helper function to streamline the Docker build process. # Arguments: @@ -120,12 +120,6 @@ command_exists() { # 3. Tags (space-separated string of tags) # 4. Cache Bust to force rebuild. # 5. Additional build arguments (optional) -# -# The function: -# - Validates the Dockerfile existence. -# - Prints a build message and runs 'docker build' with specified args. -# - Applies a CACHEBUST build-arg by default to help with cache invalidation. -# - Prints success or error messages accordingly. build_docker_image() { local dockerfile="${1}" local image_name="${2}" @@ -135,36 +129,82 @@ build_docker_image() { # Validate that the Dockerfile exists if [[ ! -f "./dockerfiles/${dockerfile}" ]]; then - printf "Error: Dockerfile not found at '%s'. Skipping build of %s.\n" "${dockerfile}" "${image_name}" >&2 + print_error "Dockerfile not found at '${dockerfile}'. Skipping build of ${image_name}." return 1 fi - printf "Building image: %s from Dockerfile: %s\n" "${image_name}" "${dockerfile}" + echo "Building image: ${image_name} from Dockerfile: ${dockerfile}" if ! docker build \ --build-arg CACHEBUST="${cache_bust}" ${build_args} \ --file "./dockerfiles/${dockerfile}" \ $(for tag in ${tags}; do printf -- "--tag ${image_name}:%s " "${tag}"; done) \ .; then - printf "Error: Failed to build image %s.\n" "${image_name}" >&2 + print_error "Failed to build image ${image_name}." return 1 fi - printf "Successfully built: %s\n\n" "${image_name}" + echo "Successfully built: ${image_name}" + echo +} + +# ----------------------------------------------------------------------------------- +# Function: build_nextcloud_app_image +# Purpose: Build a Nextcloud image with a specific app installed +# Arguments: +# 1. app_name - Name of the app (e.g., "sciencemesh") +# 2. app_repo - Git repository URL +# 3. app_branch - Git branch (default: "main") +# 4. app_build_cmd - Build command if required (optional) +# 5. init_script - Path to initialization script (optional) +# 6. nextcloud_version - Nextcloud version to use as base +# 7. image_tag_suffix - Suffix for the image tag (optional) +# ----------------------------------------------------------------------------------- +build_nextcloud_app_image() { + local app_name="${1}" + local app_repo="${2}" + local app_branch="${3:-main}" + local app_build_cmd="${4:-}" + local init_script="${5:-}" + local nextcloud_version="${6}" + local image_tag_suffix="${7:-${app_name}}" + + local build_args="" + + # Construct build arguments string + build_args="--build-arg NEXTCLOUD_VERSION=${nextcloud_version}" + build_args="${build_args} --build-arg APP_NAME=${app_name}" + build_args="${build_args} --build-arg APP_REPO=${app_repo}" + build_args="${build_args} --build-arg APP_BRANCH=${app_branch}" + + # Add optional build arguments if provided + [[ -n "${app_build_cmd}" ]] && build_args="${build_args} --build-arg APP_BUILD_CMD=${app_build_cmd}" + [[ -n "${init_script}" ]] && build_args="${build_args} --build-arg INIT_SCRIPT=${init_script}" + + # Construct the image tag + local image_tag="${nextcloud_version}-${image_tag_suffix}" + + echo "Building Nextcloud app image: ${app_name} (${image_tag})" + if ! docker build \ + ${build_args} \ + --file "./dockerfiles/nextcloud-app.Dockerfile" \ + --tag "pondersource/nextcloud:${image_tag}" \ + .; then + print_error "Failed to build Nextcloud app image: ${app_name}" + return 1 + fi + + echo "Successfully built Nextcloud app image: ${app_name}" + echo } # ----------------------------------------------------------------------------------- -# Function: main -# Purpose: Main function to manage the flow of the script. +# Main Execution # ----------------------------------------------------------------------------------- main() { - # Initialize environment. + # Initialize environment and source utilities initialize_environment - # ----------------------------------------------------------------------------------- # Enable Docker BuildKit (Optional) - # ----------------------------------------------------------------------------------- - # Allow enabling or disabling BuildKit via the first script argument. - # Default: BuildKit enabled (value 1). USE_BUILDKIT=${1:-1} export DOCKER_BUILDKIT="${USE_BUILDKIT}" @@ -173,18 +213,12 @@ main() { # ----------------------------------------------------------------------------------- # Build Images # ----------------------------------------------------------------------------------- - # Below is a list of images to build along with their Dockerfiles and tags. - # Modify these as necessary to fit your environment and requirements. - # OCM Stub build_docker_image ocmstub.Dockerfile pondersource/ocmstub "v1.0.0 latest" DEFAULT # Revad build_docker_image revad.Dockerfile pondersource/revad "latest" DEFAULT - # PHP Base - # build_docker_image php-base.Dockerfile pondersource/php-base "latest" DEFAULT - # Nextcloud Base build_docker_image nextcloud-base.Dockerfile pondersource/nextcloud-base "latest" DEFAULT @@ -203,15 +237,10 @@ main() { # Iterate over the array of versions for i in "${!nextcloud_versions[@]}"; do version="${nextcloud_versions[i]}" - + tags="${version}" # If this is the first element (index 0), also add the "latest" tag - if [[ "$i" -eq 0 ]]; then - tags="${version} latest" - else - tags="${version}" - fi + [[ "$i" -eq 0 ]] && tags="${version} latest" - # Build the Docker image with the determined tags and build-arg build_docker_image \ nextcloud.Dockerfile \ pondersource/nextcloud \ @@ -220,12 +249,29 @@ main() { "--build-arg NEXTCLOUD_BRANCH=${version}" done - # nextcloud Variants - # build_docker_image nextcloud-solid.Dockerfile pondersource/nextcloud-solid "latest" DEFAULT - # build_docker_image nextcloud-sciencemesh.Dockerfile pondersource/nextcloud-sciencemesh "latest" DEFAULT + # Build Nextcloud App Variants + # ScienceMesh + build_nextcloud_app_image \ + "sciencemesh" \ + "https://github.com/sciencemesh/nc-sciencemesh" \ + "nextcloud" \ + "make" \ + "./scripts/init/nc-sm.sh" \ + "v27.1.11" \ + "sm" + + # Example: Solid (commented out) + # build_nextcloud_app_image \ + # "solid" \ + # "https://github.com/pondersource/solid-nextcloud" \ + # "main" \ + # "make" \ + # "./scripts/init/solid.sh" \ + # "v27.1.11" \ + # "solid" # ownCloud Base - build_docker_image owncloud-base.Dockerfile pondersource/owncloud-base "latest" DEFAULT + build_docker_image owncloud-base.Dockerfile pondersource/owncloud-base "latest" DEFAULT # ownCloud Versions # The first element in this array is considered the "latest". @@ -234,15 +280,9 @@ main() { # Iterate over the array of versions for i in "${!owncloud_versions[@]}"; do version="${owncloud_versions[i]}" + tags="${version}" + [[ "$i" -eq 0 ]] && tags="${version} latest" - # If this is the first element (index 0), also add the "latest" tag - if [[ "$i" -eq 0 ]]; then - tags="${version} latest" - else - tags="${version}" - fi - - # Build the Docker image with the determined tags and build-arg build_docker_image \ owncloud.Dockerfile \ pondersource/owncloud \ @@ -251,19 +291,8 @@ main() { "--build-arg OWNCLOUD_BRANCH=${version}" done - # OwnCloud Variants - # build_docker_image owncloud-sciencemesh.Dockerfile pondersource/owncloud-sciencemesh "latest" DEFAULT - # build_docker_image owncloud-surf-trashbin.Dockerfile pondersource/owncloud-surf-trashbin "latest" DEFAULT - # build_docker_image owncloud-token-based-access.Dockerfile pondersource/owncloud-token-based-access "latest" DEFAULT - # build_docker_image owncloud-opencloudmesh.Dockerfile pondersource/owncloud-opencloudmesh "latest" DEFAULT - # build_docker_image owncloud-federatedgroups.Dockerfile pondersource/owncloud-federatedgroups "latest" DEFAULT - # build_docker_image owncloud-ocm-test-suite.Dockerfile pondersource/owncloud-ocm-test-suite "latest" DEFAULT - - # ----------------------------------------------------------------------------------- - # Completion Message - # ----------------------------------------------------------------------------------- - printf "All builds attempted.\n" - printf "Check the above output for any build failures or errors.\n" + echo "All builds attempted." + echo "Check the above output for any build failures or errors." } # ----------------------------------------------------------------------------------- From f41fc00883d1e9320bcaa92cd4b4dea3d96cb2e6 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 10:41:54 +0330 Subject: [PATCH 148/184] add: sciencemesh utility scripts --- scripts/utils/container/mehsdir.sh | 17 ++++++++++++++ scripts/utils/container/reva.sh | 28 +++++++++++++++++++++++ scripts/utils/container/sciencemesh.sh | 31 ++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 scripts/utils/container/mehsdir.sh create mode 100644 scripts/utils/container/reva.sh create mode 100644 scripts/utils/container/sciencemesh.sh diff --git a/scripts/utils/container/mehsdir.sh b/scripts/utils/container/mehsdir.sh new file mode 100644 index 00000000..ff1e9939 --- /dev/null +++ b/scripts/utils/container/mehsdir.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +create_meshdir() { + local image="${1}" + local tag="${2}" + + # Start Mesh Directory + run_quietly_if_ci echo "Starting Mesh Directory..." + + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="meshdir.docker" \ + -e HOST="meshdir" \ + "${image}:${tag}" || error_exit "Failed to start meshdir." + + # Wait for EFSS port to open + run_quietly_if_ci wait_for_port "meshdir.docker" 443 +} diff --git a/scripts/utils/container/reva.sh b/scripts/utils/container/reva.sh new file mode 100644 index 00000000..e64451bf --- /dev/null +++ b/scripts/utils/container/reva.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Function: create_reva +# Purpose: Create a Reva container for the specified EFSS platform and instance. +# Arguments: +# $1 - EFSS platform (e.g., nextcloud). +# $2 - Instance number. +# $3 - Reva image. +# $4 - Reva tag. +# ----------------------------------------------------------------------------------- +create_reva() { + local platform="${1}" + local number="${2}" + local image="${3}" + local tag="${4}" + + run_quietly_if_ci echo "Creating Reva instance: ${platform} ${number}" + + # Start Reva container + run_docker_container --detach --network="${DOCKER_NETWORK}" \ + --name="reva${platform}${number}.docker" \ + -e HOST="reva${platform}${number}" \ + "${image}:${tag}" || error_exit "Failed to start Reva container for ${platform} ${number}." + + # Wait for Reva port to open (assuming Reva uses port 19000) + wait_for_port "reva${platform}${number}.docker" 19000 +} diff --git a/scripts/utils/container/sciencemesh.sh b/scripts/utils/container/sciencemesh.sh new file mode 100644 index 00000000..9cf1b1ae --- /dev/null +++ b/scripts/utils/container/sciencemesh.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Function: configure_sciencemesh +# Purpose: Configure ScienceMesh settings for the EFSS platform. +# Arguments: +# $1 - EFSS platform (e.g., nextcloud). +# $2 - Instance number. +# $3 - IOP URL. +# $4 - Reva shared secret. +# $5 - Mesh directory URL. +# $6 - Invite manager API key. +# ----------------------------------------------------------------------------------- +configure_sciencemesh() { + local platform="${1}" + local number="${2}" + local iop_url="${3}" + local reva_shared_secret="${4}" + local mesh_directory_url="${5}" + local invite_manager_apikey="${6}" + + run_quietly_if_ci echo "Configuring ScienceMesh for ${platform} ${number}" + + local mysql_cmd="docker exec maria${platform}${number}.docker mariadb -u root -p${MARIADB_ROOT_PASSWORD} efss" + + # Insert ScienceMesh configuration into the database + run_quietly_if_ci $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', '${iop_url}');" + run_quietly_if_ci $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', '${reva_shared_secret}');" + run_quietly_if_ci $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', '${mesh_directory_url}');" + run_quietly_if_ci $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', '${invite_manager_apikey}');" +} From a17edd600366cfb3582f9aa89cd23e75e6bc76b8 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 10:42:31 +0330 Subject: [PATCH 149/184] add: dockerfile needed init script for sm --- docker/scripts/init/nc-sm.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docker/scripts/init/nc-sm.sh diff --git a/docker/scripts/init/nc-sm.sh b/docker/scripts/init/nc-sm.sh new file mode 100644 index 00000000..1c0442cb --- /dev/null +++ b/docker/scripts/init/nc-sm.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +ln -sf /ponder/apps/sciencemesh /var/html/www/apps/sciencemesh + +php console.php app:enable --force sciencemesh From be119dd7c02c332ef1b9177cad6e69b86d4b442d Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 10:43:50 +0330 Subject: [PATCH 150/184] add: initial nc-nc sm test --- .../invite-link/nextcloud-nextcloud.sh | 509 +++--------------- 1 file changed, 72 insertions(+), 437 deletions(-) diff --git a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh index 0e21b13e..33c21c46 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh @@ -39,323 +39,76 @@ set -euo pipefail # ----------------------------------------------------------------------------------- # Default versions -DEFAULT_EFSS_VERSION="v27.1.11" -DEFAULT_SCRIPT_MODE="dev" -DEFAULT_BROWSER_PLATFORM="electron" - -# Docker network name -DOCKER_NETWORK="testnet" - -# MariaDB root password -MARIADB_ROOT_PASSWORD="eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" - -# Paths to required directories -TEMP_DIR="temp" -TLS_CA_DIR="docker/tls/certificate-authority" -TLS_CERT_DIR="docker/tls/certificates" - -# 3rd party containerS -CYPRESS_REPO=cypress/included -CYPRESS_TAG=13.13.1@sha256:e9bb8aa3e4cca25867c1bdb09bd0a334957fc26ec25239534e6909697efb297e -FIREFOX_REPO=jlesage/firefox -FIREFOX_TAG=v24.11.1@sha256:ea3ef3febbfadb876955c2eaff5dde4772f70676cd318e0e3706c5ddc0fd9e68 -MARIADB_REPO=mariadb -MARIADB_TAG=11.6.2@sha256:0a620383fe05d20b3cc7510ebccc6749f83f1b0f97f3030d10dd2fa199371f07 -VNC_REPO=theasp/novnc -VNC_TAG=latest@sha256:26dcdccd36e5a6f6eb93beb76c8a74d5a5120a58184433f948428bb018d54c58 - - -# ----------------------------------------------------------------------------------- -# Utility Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: print_error -# Purpose: Print an error message to stderr. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -print_error() { - local message="${1}" - printf "Error: %s\n" "${message}" >&2 -} - -# ----------------------------------------------------------------------------------- -# Function: error_exit -# Purpose: Print an error message and exit with code 1. -# Arguments: -# $1 - The error message to display. -# ----------------------------------------------------------------------------------- -error_exit() { - print_error "${1}" - exit 1 -} +DEFAULT_EFSS_1_VERSION="v27.1.11-sm" +DEFAULT_EFSS_2_VERSION="v27.1.11-sm" # ----------------------------------------------------------------------------------- # Function: resolve_script_dir -# Purpose: Resolves the absolute path of the script's directory, handling symlinks. -# Returns: -# The absolute path to the script's directory. +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. # ----------------------------------------------------------------------------------- resolve_script_dir() { local source="${BASH_SOURCE[0]}" - local dir + + # Follow symbolic links until we get the real file location while [ -L "${source}" ]; do + # Get the directory path where the symlink is located dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to source="$(readlink "${source}")" - # Resolve relative symlink + # If the source was a relative symlink, convert it to an absolute path [[ "${source}" != /* ]] && source="${dir}/${source}" done - dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" - printf "%s" "${dir}" + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" } # ----------------------------------------------------------------------------------- # Function: initialize_environment -# Purpose: Initialize the environment and set global variables. -# ----------------------------------------------------------------------------------- -initialize_environment() { - local script_dir - script_dir="$(resolve_script_dir)" - cd "${script_dir}/../../.." || error_exit "Failed to change directory to the script root." - ENV_ROOT="$(pwd)" - export ENV_ROOT="${ENV_ROOT}" - - # Ensure required commands are available - for cmd in docker; do - if ! command_exists "${cmd}"; then - error_exit "Required command '${cmd}' is not available. Please install it and try again." - fi - done -} - -# ----------------------------------------------------------------------------------- -# Function: wait_for_port -# Purpose: Wait for a Docker container to open a specific port. +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# # Arguments: -# $1 - The name of the Docker container. -# $2 - The port number to check. +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" # ----------------------------------------------------------------------------------- -wait_for_port() { - local container="${1}" - local port="${2}" - - run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do - run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." - sleep 1 - done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." -} - -# ----------------------------------------------------------------------------------- -# Function: run_quietly_if_ci -# Purpose: Run a command, suppressing stdout in CI mode. -# Arguments: -# $@ - The command and arguments to execute. -# ----------------------------------------------------------------------------------- -run_quietly_if_ci() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT else - "$@" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: run_docker_container -# Purpose: Start a Docker container with the provided arguments. -# Arguments: -# $@ - Docker run command arguments -# ----------------------------------------------------------------------------------- -run_docker_container() { - run_quietly_if_ci docker run "$@" || error_exit "Failed to start Docker container: $*" -} - - -# ----------------------------------------------------------------------------------- -# Function: remove_directory -# Purpose: Safely remove a directory if it exists. -# Arguments: -# $1 - Directory path -# ----------------------------------------------------------------------------------- -remove_directory() { - local dir="${1}" - if [ -d "${dir}" ]; then - run_quietly_if_ci rm -rf "${dir}" || error_exit "Failed to remove directory: ${dir}" - fi -} - -# ----------------------------------------------------------------------------------- -# Function: command_exists -# Purpose: Check if a command exists on the system. -# Arguments: -# $1 - The command to check. -# Returns: -# 0 if the command exists, 1 otherwise. -# ----------------------------------------------------------------------------------- - -command_exists() { - command -v "${1}" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Setup Functions -# ----------------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------------- -# Function: create_nextcloud -# Purpose: Create a Nextcloud container with a MariaDB backend. -# Arguments: -# $1 - Instance number. -# $2 - Admin username. -# $3 - Admin password. -# $4 - Initialization script filename. -# $5 - EFSS platform version (optional). -# ----------------------------------------------------------------------------------- -create_nextcloud() { - local number="${1}" - local user="${2}" - local password="${3}" - local init_script="${4}" - local version="${6:-$DEFAULT_EFSS_VERSION}" - - run_quietly_if_ci echo "Creating EFSS instance: nextcloud ${number}" - - # Validate that the init script exists - if [ ! -f "${ENV_ROOT}/${TEMP_DIR}/${init_script}" ]; then - error_exit "Initialization script not found: ${ENV_ROOT}/${TEMP_DIR}/${init_script}" + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 fi - - # Start MariaDB container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="marianextcloud${number}.docker" \ - -e MARIADB_ROOT_PASSWORD="${MARIADB_ROOT_PASSWORD}" \ - "${MARIADB_REPO}":"${MARIADB_TAG}" \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed || error_exit "Failed to start MariaDB container for nextcloud ${number}." - - # Wait for MariaDB port to open - wait_for_port "marianextcloud${number}.docker" 3306 - - # Start EFSS container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="nextcloud${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="nextcloud${number}" \ - -e DBHOST="marianextcloud${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/${TLS_CERT_DIR}:/certificates" \ - -v "${ENV_ROOT}/${TLS_CA_DIR}:/certificate-authority" \ - -v "${ENV_ROOT}/${TEMP_DIR}/${init_script}":"/init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh":"/entrypoint.sh" \ - "pondersource/dev-stock-nextcloud-sciencemesh:latest" || error_exit "Failed to start EFSS container for nextcloud ${number}." - - # Wait for EFSS port to open - run_quietly_if_ci wait_for_port "nextcloud${number}.docker" 443 - - # Install and update certificates inside the EFSS container - run_quietly_if_ci docker exec "nextcloud${number}.docker" sh -c "cp /certificates/*.crt /usr/local/share/ca-certificates/ || true" - run_quietly_if_ci docker exec "nextcloud${number}.docker" sh -c "update-ca-certificates" || error_exit "Failed to update CA certificates in ${platform} ${number}." - # Run the initialization script inside EFSS - run_quietly_if_ci docker exec -u www-data "nextcloud${number}.docker" sh -c "/init.sh" || error_exit "Initialization script failed for ${platform} ${number}." -} - -# ----------------------------------------------------------------------------------- -# Function: create_reva -# Purpose: Create a Reva container for the specified EFSS platform and instance. -# Arguments: -# $1 - EFSS platform (e.g., nextcloud). -# $2 - Instance number. -# ----------------------------------------------------------------------------------- -create_reva() { - local platform="${1}" - local number="${2}" - - run_quietly_if_ci echo "Creating Reva instance: ${platform} ${number}" - - # Ensure Reva scripts are executable - run_quietly_if_ci chmod +x "${ENV_ROOT}/${TEMP_DIR}/reva/"{run.sh,kill.sh,entrypoint.sh} || error_exit "Failed to make Reva scripts executable." - - # Start Reva container - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/${TLS_CERT_DIR}:/certificates" \ - -v "${ENV_ROOT}/${TLS_CA_DIR}:/certificate-authority" \ - -v "${ENV_ROOT}/${TEMP_DIR}/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/${TEMP_DIR}/reva/run.sh":"/usr/bin/run.sh" \ - -v "${ENV_ROOT}/${TEMP_DIR}/reva/kill.sh":"/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/${TEMP_DIR}/reva/entrypoint.sh":"/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad || error_exit "Failed to start Reva container for ${platform} ${number}." - - # Wait for Reva port to open (assuming Reva uses port 19000) - wait_for_port "reva${platform}${number}.docker" 19000 -} - - -# ----------------------------------------------------------------------------------- -# Function: configure_sciencemesh -# Purpose: Configure ScienceMesh settings for the EFSS platform. -# Arguments: -# $1 - EFSS platform (e.g., nextcloud). -# $2 - Instance number. -# ----------------------------------------------------------------------------------- -configure_sciencemesh() { - local platform="${1}" - local number="${2}" - - run_quietly_if_ci echo "Configuring ScienceMesh for ${platform} ${number}" - - local mysql_cmd="docker exec maria${platform}${number}.docker mariadb -u root -p${MARIADB_ROOT_PASSWORD} efss" - - # Insert ScienceMesh configuration into the database - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 -} - -# ----------------------------------------------------------------------------------- -# Function: parse_arguments -# Purpose: Parse command-line arguments and set global variables. -# Arguments: -# $@ - Command-line arguments -# ----------------------------------------------------------------------------------- -parse_arguments() { - EFSS_PLATFORM_1_VERSION="${1:-$DEFAULT_EFSS_VERSION}" - EFSS_PLATFORM_2_VERSION="${2:-$DEFAULT_EFSS_VERSION}" - SCRIPT_MODE="${3:-$DEFAULT_SCRIPT_MODE}" - BROWSER_PLATFORM="${4:-$DEFAULT_BROWSER_PLATFORM}" -} - -# ----------------------------------------------------------------------------------- -# Function: validate_files -# Purpose: Validate that required files and directories exist. -# ----------------------------------------------------------------------------------- -validate_files() { - # Check if TLS certificate files exist - if [ ! -d "${ENV_ROOT}/${TLS_CERT_DIR}" ]; then - error_exit "TLS certificates directory not found: ${ENV_ROOT}/${TLS_CERT_DIR}" - fi - if [ ! -d "${ENV_ROOT}/${TLS_CA_DIR}" ]; then - error_exit "TLS certificate authority directory not found: ${ENV_ROOT}/${TLS_CA_DIR}" - fi - - # Check if Firefox certificate files exist - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" ]; then - error_exit "Firefox cert9.db file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db" - fi - if [ ! -f "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" ]; then - error_exit "Firefox cert_override.txt file not found: ${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt" - fi - - # Check if Cypress configuration exists - if [ ! -f "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" ]; then - error_exit "Cypress configuration file not found: ${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } @@ -365,149 +118,31 @@ validate_files() { main() { # Initialize environment and parse arguments - initialize_environment - parse_arguments "$@" - validate_files - - # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" - # Copy init files. - cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/${TEMP_DIR}/" - cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/${TEMP_DIR}/reva/configs" - cp -f "${ENV_ROOT}/docker/scripts/ocmstub/index.js" "${ENV_ROOT}/${TEMP_DIR}/index.js" - cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-sciencemesh.sh" "${ENV_ROOT}/${TEMP_DIR}/nextcloud.sh" - # Remove unnecessary configs. - rm "${ENV_ROOT}/${TEMP_DIR}/reva/configs/sciencemesh-apps-codimd.toml" - rm "${ENV_ROOT}/${TEMP_DIR}/reva/configs/sciencemesh-apps-collabora.toml" - - # Clean up previous resources and ensure Docker network exists - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'. Continuing without cleanup." - fi - - if ! docker network inspect "${DOCKER_NETWORK}" >/dev/null 2>&1; then - docker network create "${DOCKER_NETWORK}" >/dev/null 2>&1 || error_exit "Failed to create Docker network '${DOCKER_NETWORK}'." - fi - - # Create Nextcloud containers - # #id #username #password #init_filename #nextcloud_version - create_nextcloud 1 "einstein" "relativity" "nextcloud.sh" "${EFSS_PLATFORM_1_VERSION}" - create_nextcloud 2 "michiel" "dejong" "nextcloud.sh" "${EFSS_PLATFORM_2_VERSION}" - + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + # Create Reva containers - create_reva "nextcloud" 1 - create_reva "nextcloud" 2 - + create_reva "nextcloud" 1 pondersource/revad latest + create_reva "nextcloud" 2 pondersource/revad latest + # Configure ScienceMesh integration - configure_sciencemesh "nextcloud" 1 - configure_sciencemesh "nextcloud" 2 - - # Start Mesh Directory - run_quietly_if_ci echo "Starting Mesh Directory..." - run_docker_container --detach --network="$DOCKER_NETWORK" \ - --name="meshdir.docker" \ - -e HOST="meshdir" \ - -v "${ENV_ROOT}/${TEMP_DIR}/index.js:/ocmstub/index.js" \ - pondersource/dev-stock-ocmstub - + configure_sciencemesh "nextcloud" 1 "https://revanextcloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + configure_sciencemesh "nextcloud" 2 "https://revanextcloud2.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + + # Start Mesh Directory + create_meshdir pondersource/ocmstub v1.0.0 + + if [ "${SCRIPT_MODE}" = "dev" ]; then - echo "Setting up development environment..." - - # Start Firefox container - run_quietly_if_ci echo "Starting Firefox container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="firefox" \ - -p 5800:5800 \ - --shm-size=2g \ - -e USER_ID="$(id -u)" \ - -e GROUP_ID="$(id -g)" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - "${FIREFOX_REPO}":"${FIREFOX_TAG}" - - # Start VNC Server container - run_quietly_if_ci echo "Starting VNC Server..." - local x11_socket="${ENV_ROOT}/${TEMP_DIR}/.X11-unix" - # Ensure previous socket files are removed - remove_directory "${x11_socket}" - mkdir -p "${x11_socket}" - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="vnc-server" \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${x11_socket}:/tmp/.X11-unix" \ - "${VNC_REPO}":"${VNC_TAG}" - - # Start Cypress container - echo "Starting Cypress container..." - run_docker_container --detach --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -e DISPLAY="vnc-server:0.0" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${x11_socket}:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - open --project . - - # Display setup instructions - echo "" - echo "Development environment setup complete." - echo "Access the following URLs in your browser:" - echo " Cypress inside VNC Server -> http://localhost:5700/vnc.html" - echo " Embedded Firefox -> http://localhost:5800" - echo "Note:" - echo " Scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "" - echo "Log in to EFSS platforms using the following credentials:" - echo " https://nextcloud1.docker (username: einstein, password: relativity)" - echo " https://nextcloud2.docker (username: michiel, password: dejong)" - + run_dev \ + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://nextcloud2.docker (username: michiel, password: dejong)" else - echo "Running tests in CI mode..." - - # Cypress configuration file - local cypress_config="${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - - # Adjust Cypress configurations for non-default browser platforms - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${cypress_config}" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${cypress_config}" - fi - - # Extract major version numbers for EFSS platforms - local P1_VER="${EFSS_PLATFORM_1_VERSION%%.*}" - local P2_VER="${EFSS_PLATFORM_2_VERSION%%.*}" - - # Run Cypress tests in headless mode - echo "Running Cypress tests..." - docker run --network="${DOCKER_NETWORK}" \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - "${CYPRESS_REPO}":"${CYPRESS_TAG}" \ - cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/nextcloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" || error_exit "Cypress tests failed." - - # Revert Cypress configuration changes - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${cypress_config}" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${cypress_config}" - fi - - # Perform cleanup after CI tests - echo "Cleaning up test environment..." - if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then - "${ENV_ROOT}/scripts/clean.sh" "no" - else - print_error "Cleanup script not found or not executable at '${ENV_ROOT}/scripts/clean.sh'." - fi + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi } From b600a18cdaf18d376e45c755808b2fde49de0083 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 11:08:26 +0330 Subject: [PATCH 151/184] fix: typo in command name --- docker/scripts/reva/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/reva/entrypoint.sh b/docker/scripts/reva/entrypoint.sh index 7644eb6c..f55a3edf 100755 --- a/docker/scripts/reva/entrypoint.sh +++ b/docker/scripts/reva/entrypoint.sh @@ -11,7 +11,7 @@ echo "127.0.0.1 ${HOST}.docker" >> /etc/hosts touch /var/log/revad.log # run revad. -run.sh +init.sh # This will exec the CMD from your Dockerfile, i.e. "npm start" exec "$@" From b6cc8fa3033ea88c3e91658f6e1657c5462a10fa Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 11:23:44 +0330 Subject: [PATCH 152/184] add: support for disabling configs on reva --- .../invite-link/nextcloud-nextcloud.sh | 8 ++--- docker/scripts/reva/init.sh | 31 +++++++++++++++++++ scripts/utils/container/reva.sh | 3 ++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh index 33c21c46..8e0f8519 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh @@ -125,9 +125,10 @@ main() { create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" create_nextcloud 2 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" - # Create Reva containers - create_reva "nextcloud" 1 pondersource/revad latest - create_reva "nextcloud" 2 pondersource/revad latest + # Create Reva containers with disabled app configs + local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" + create_reva "nextcloud" 1 pondersource/revad latest "${disabled_configs}" + create_reva "nextcloud" 2 pondersource/revad latest "${disabled_configs}" # Configure ScienceMesh integration configure_sciencemesh "nextcloud" 1 "https://revanextcloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" @@ -136,7 +137,6 @@ main() { # Start Mesh Directory create_meshdir pondersource/ocmstub v1.0.0 - if [ "${SCRIPT_MODE}" = "dev" ]; then run_dev \ "https://nextcloud1.docker (username: einstein, password: relativity)" \ diff --git a/docker/scripts/reva/init.sh b/docker/scripts/reva/init.sh index 1f0a0ef5..4aca4581 100755 --- a/docker/scripts/reva/init.sh +++ b/docker/scripts/reva/init.sh @@ -51,6 +51,7 @@ TLS_DIR="/tls" # Directory where certificates/keys a CERTS_DIR="/certificates" # Directory containing optional certificate files CA_DIR="/certificate-authority" # Directory containing optional CA certificate/key files HOST="${HOST:-localhost}" # Hostname for the environment, defaults to "localhost" +DISABLED_CONFIGS="${DISABLED_CONFIGS:-}" # Space-separated list of config files to disable # ----------------------------------------------------------------------------------- # Function: create_directory @@ -102,6 +103,32 @@ populate_reva_binaries() { fi } +# ----------------------------------------------------------------------------------- +# Function: disable_config_files +# Purpose: Remove specified configuration files from /etc/revad. +# Behavior: +# - Takes a space-separated list of config files from DISABLED_CONFIGS. +# - Removes each specified file from /etc/revad if it exists. +# Returns: +# 0 on success, does not fail if files don't exist. +# ----------------------------------------------------------------------------------- +disable_config_files() { + if [[ -z "$DISABLED_CONFIGS" ]]; then + return 0 + fi + + printf "Disabling specified configuration files...\n" + for config in $DISABLED_CONFIGS; do + local config_path="$REVA_CONFIG_DIR/$config" + if [[ -f "$config_path" ]]; then + printf "Removing config file: %s\n" "$config" + rm -f "$config_path" + else + printf "Config file not found: %s\n" "$config" + fi + done +} + # ----------------------------------------------------------------------------------- # Function: prepare_configuration # Purpose: Copy Reva configuration files to /etc/revad and replace placeholders. @@ -110,6 +137,7 @@ populate_reva_binaries() { # - Remove any existing /etc/revad directory. # - Copy /configs/revad to /etc/revad. # - Replace placeholder hostnames in .toml files with $HOST.docker or derived host. +# - Disable specified configuration files. # Returns: # 0 on success, 1 on failure. # ----------------------------------------------------------------------------------- @@ -133,6 +161,9 @@ prepare_configuration() { sed -i "s/localhost/${HOST}.docker/g" "$REVA_CONFIG_DIR"/*.toml || true sed -i "s/your.efss.org/${HOST//reva/}.docker/g" "$REVA_CONFIG_DIR"/*.toml || true sed -i "s/your.nginx.org/${HOST//reva/}.docker/g" "$REVA_CONFIG_DIR"/*.toml || true + + # Disable specified configuration files + disable_config_files } # ----------------------------------------------------------------------------------- diff --git a/scripts/utils/container/reva.sh b/scripts/utils/container/reva.sh index e64451bf..ad148bb1 100644 --- a/scripts/utils/container/reva.sh +++ b/scripts/utils/container/reva.sh @@ -8,12 +8,14 @@ # $2 - Instance number. # $3 - Reva image. # $4 - Reva tag. +# $5 - Disabled configs (optional, space-separated list of config files to disable). # ----------------------------------------------------------------------------------- create_reva() { local platform="${1}" local number="${2}" local image="${3}" local tag="${4}" + local disabled_configs="${5:-}" run_quietly_if_ci echo "Creating Reva instance: ${platform} ${number}" @@ -21,6 +23,7 @@ create_reva() { run_docker_container --detach --network="${DOCKER_NETWORK}" \ --name="reva${platform}${number}.docker" \ -e HOST="reva${platform}${number}" \ + -e DISABLED_CONFIGS="${disabled_configs}" \ "${image}:${tag}" || error_exit "Failed to start Reva container for ${platform} ${number}." # Wait for Reva port to open (assuming Reva uses port 19000) From 1f7dee94bbb1338ecd62cf708b1e417186c39ae8 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 11:32:42 +0330 Subject: [PATCH 153/184] fix: typo --- docker/scripts/init/nc-sm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/init/nc-sm.sh b/docker/scripts/init/nc-sm.sh index 1c0442cb..4c42e122 100644 --- a/docker/scripts/init/nc-sm.sh +++ b/docker/scripts/init/nc-sm.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -ln -sf /ponder/apps/sciencemesh /var/html/www/apps/sciencemesh +ln -sf /ponder/apps/sciencemesh /var/www/html/apps/sciencemesh php console.php app:enable --force sciencemesh From 2d17ffa6625f3c12fc6c0b02398b2b33b03e0ca7 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 11:41:13 +0330 Subject: [PATCH 154/184] fix: link creation --- docker/scripts/init/nc-sm.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docker/scripts/init/nc-sm.sh b/docker/scripts/init/nc-sm.sh index 4c42e122..63c81994 100644 --- a/docker/scripts/init/nc-sm.sh +++ b/docker/scripts/init/nc-sm.sh @@ -1,5 +1,18 @@ #!/usr/bin/env bash -ln -sf /ponder/apps/sciencemesh /var/www/html/apps/sciencemesh +# Target paths +APP_SOURCE="/ponder/apps/sciencemesh" +APP_TARGET="/var/www/html/apps/sciencemesh" -php console.php app:enable --force sciencemesh +# Remove existing directory or symlink if it exists +if [ -e "${APP_TARGET}" ] || [ -L "${APP_TARGET}" ]; then + rm -rf "${APP_TARGET}" +fi + +# Create new symlink +ln -sf "${APP_SOURCE}" "${APP_TARGET}" + +# hack sciencemesh version :=) +sed -i -e 's/min-version="28"/min-version="27"/g' "${APP_TARGET}/appinfo/info.xml" + +php console.php app:enable sciencemesh From 93b4223fdf75506a87426ee0e8341f9449edcc1e Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Mon, 27 Jan 2025 11:44:59 +0330 Subject: [PATCH 155/184] add: maximum timeout for wait_for_port --- scripts/utils/environment.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/utils/environment.sh b/scripts/utils/environment.sh index c6e4df65..84d408e3 100644 --- a/scripts/utils/environment.sh +++ b/scripts/utils/environment.sh @@ -33,11 +33,24 @@ remove_directory() { wait_for_port() { local container="${1}" local port="${2}" + local timeout=60 # Maximum wait time in seconds + local start_time=$(date +%s) run_quietly_if_ci echo "Waiting for port ${port} on container ${container}..." - until docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; do + while true; do + if docker exec "${container}" sh -c "ss -tulpn | grep -q 'LISTEN.*:${port}'" >/dev/null 2>&1; then + run_quietly_if_ci echo "Port ${port} is now open on ${container}." + return 0 + fi + + current_time=$(date +%s) + elapsed_time=$((current_time - start_time)) + + if [ ${elapsed_time} -ge ${timeout} ]; then + error_exit "Timeout waiting for port ${port} on container ${container} after ${timeout} seconds" + fi + run_quietly_if_ci echo "Port ${port} not open yet on ${container}. Retrying..." sleep 1 done - run_quietly_if_ci echo "Port ${port} is now open on ${container}." } From c0811d2dec5f6c8fcf4a95bda786e392bed731e7 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 14:04:25 +0330 Subject: [PATCH 156/184] add: changed names for the sm nextcloud --- ...extcloud-sm-v27-to-nextcloud-sm-v27.cy.js} | 0 ...tcloud.sh => nextcloud-sm-nextcloud-sm.sh} | 0 docker/build/all.sh | 2 +- docker/scripts/init/nc-sm.sh | 18 ------------- docker/scripts/init/nextcloud-sciencemesh.sh | 25 ++++++++----------- docker/scripts/switch-php.sh | 22 ---------------- 6 files changed, 11 insertions(+), 56 deletions(-) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{nextcloud-v27-to-nextcloud-v27.cy.js => nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js} (100%) rename dev/ocm-test-suite/invite-link/{nextcloud-nextcloud.sh => nextcloud-sm-nextcloud-sm.sh} (100%) mode change 100755 => 100644 delete mode 100644 docker/scripts/init/nc-sm.sh delete mode 100755 docker/scripts/switch-php.sh diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-nextcloud-v27.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js diff --git a/dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh old mode 100755 new mode 100644 similarity index 100% rename from dev/ocm-test-suite/invite-link/nextcloud-nextcloud.sh rename to dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh diff --git a/docker/build/all.sh b/docker/build/all.sh index 9a85b572..61155422 100755 --- a/docker/build/all.sh +++ b/docker/build/all.sh @@ -256,7 +256,7 @@ main() { "https://github.com/sciencemesh/nc-sciencemesh" \ "nextcloud" \ "make" \ - "./scripts/init/nc-sm.sh" \ + "./scripts/init/nextcloud-sciencemesh.sh" \ "v27.1.11" \ "sm" diff --git a/docker/scripts/init/nc-sm.sh b/docker/scripts/init/nc-sm.sh deleted file mode 100644 index 63c81994..00000000 --- a/docker/scripts/init/nc-sm.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -# Target paths -APP_SOURCE="/ponder/apps/sciencemesh" -APP_TARGET="/var/www/html/apps/sciencemesh" - -# Remove existing directory or symlink if it exists -if [ -e "${APP_TARGET}" ] || [ -L "${APP_TARGET}" ]; then - rm -rf "${APP_TARGET}" -fi - -# Create new symlink -ln -sf "${APP_SOURCE}" "${APP_TARGET}" - -# hack sciencemesh version :=) -sed -i -e 's/min-version="28"/min-version="27"/g' "${APP_TARGET}/appinfo/info.xml" - -php console.php app:enable sciencemesh diff --git a/docker/scripts/init/nextcloud-sciencemesh.sh b/docker/scripts/init/nextcloud-sciencemesh.sh index 547f6712..63c81994 100755 --- a/docker/scripts/init/nextcloud-sciencemesh.sh +++ b/docker/scripts/init/nextcloud-sciencemesh.sh @@ -1,23 +1,18 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts -set -e +# Target paths +APP_SOURCE="/ponder/apps/sciencemesh" +APP_TARGET="/var/www/html/apps/sciencemesh" -php console.php maintenance:install --admin-user "${USER}" --admin-pass "${PASS}" --database "mysql" \ - --database-name "efss" --database-user "root" --database-host "${DBHOST}" \ - --database-pass "eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" -php console.php app:disable firstrunwizard +# Remove existing directory or symlink if it exists +if [ -e "${APP_TARGET}" ] || [ -L "${APP_TARGET}" ]; then + rm -rf "${APP_TARGET}" +fi -# change/add lines in config.php -sed -i "3 i\ 'allow_local_remote_servers' => true," /var/www/html/config/config.php -sed -i "8 i\ 1 => 'nc1.docker'," /var/www/html/config/config.php -sed -i "9 i\ 2 => 'nc2.docker'," /var/www/html/config/config.php -sed -i "10 i\ 3 => 'nextcloud1.docker'," /var/www/html/config/config.php -sed -i "11 i\ 4 => 'nextcloud2.docker'," /var/www/html/config/config.php -sed -i "12 i\ 5 => 'nextcloud3.docker'," /var/www/html/config/config.php -sed -i "13 i\ 6 => 'nextcloud4.docker'," /var/www/html/config/config.php +# Create new symlink +ln -sf "${APP_SOURCE}" "${APP_TARGET}" # hack sciencemesh version :=) -sed -i -e 's/min-version="28"/min-version="27"/g' /var/www/html/apps/sciencemesh/appinfo/info.xml +sed -i -e 's/min-version="28"/min-version="27"/g' "${APP_TARGET}/appinfo/info.xml" php console.php app:enable sciencemesh diff --git a/docker/scripts/switch-php.sh b/docker/scripts/switch-php.sh deleted file mode 100755 index 204c3f66..00000000 --- a/docker/scripts/switch-php.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts -set -e - -FILE="/usr/bin/php${1}" - -if [[ -n "${1}" ]]; then - if [[ -f "${FILE}" ]]; then - update-alternatives --set php "/usr/bin/php${1}" - update-alternatives --set phar "/usr/bin/phar${1}" - update-alternatives --set phar.phar "/usr/bin/phar.phar${1}" - - A2MODPHP=$(ls /etc/apache2/mods-enabled/php*.load) - a2dismod "${A2MODPHP:26:6}" - a2enmod "php${1}" - else - echo "This version is not available in this system." - fi -else - echo "You didn't provide any version number!" -fi From fb8dd10db592fcbf2e716ec7f969332b0dee4a6c Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 15:48:34 +0330 Subject: [PATCH 157/184] fix: sciencemesh test failures --- .../cypress/e2e/utils/nextcloud-v27.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js index 19dbccd2..374a56a3 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/nextcloud-v27.js @@ -406,12 +406,18 @@ export function triggerActionInFileMenuV27(fileName, actionId) { * @param {string} actionId - The action to trigger. */ export function triggerActionForFileV27(fileName, actionId) { - // Find the actions container for the file + // Find the actions container for the file and ensure it's stable getActionsForFileV27(fileName) - .find(`*[data-action="${actionId}"]`) - .should('be.visible') - .as('btn') - .click(); + .should('exist') + .and('be.visible') + .within(() => { + // Find the action button and ensure it's properly loaded + cy.get(`*[data-action="${actionId}"]`) + .should('exist') + .and('be.visible') + .and('not.be.disabled') + .click({ force: true }); + }); } /** From a7e4d06f0a51a685255e79c00a9e769d0795bbc8 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 15:48:52 +0330 Subject: [PATCH 158/184] update: nc-sm test in github --- ...-nc-v27.yml => invite-link-nc-sm-v27-nc-sm-v27.yml} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename .github/workflows/{invite-link-nc-v27-nc-v27.yml => invite-link-nc-sm-v27-nc-sm-v27.yml} (90%) diff --git a/.github/workflows/invite-link-nc-v27-nc-v27.yml b/.github/workflows/invite-link-nc-sm-v27-nc-sm-v27.yml similarity index 90% rename from .github/workflows/invite-link-nc-v27-nc-v27.yml rename to .github/workflows/invite-link-nc-sm-v27-nc-sm-v27.yml index 89f8f6ae..ab6fc993 100644 --- a/.github/workflows/invite-link-nc-v27-nc-v27.yml +++ b/.github/workflows/invite-link-nc-sm-v27-nc-sm-v27.yml @@ -1,4 +1,4 @@ -name: OCM Test Invite Link NC v27.1.11 to NC v27.1.11 +name: OCM Test Invite Link NC SM v27.1.11 to NC SM v27.1.11 # Controls when the action will run. on: @@ -28,14 +28,14 @@ jobs: matrix: sender: [ { - platform: nextcloud, - version: v27.1.11 + platform: nextcloud-sm, + version: v27.1.11-sm }, ] receiver: [ { - platform: nextcloud, - version: v27.1.11 + platform: nextcloud-sm, + version: v27.1.11-sm }, ] From 74f53032e6379a300dff71c288fa791e1d4903d8 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 15:49:09 +0330 Subject: [PATCH 159/184] add: docker pull script --- docker/pull/ocm-test-suite/nextcloud-sm.sh | 13 +++++++++++++ docker/pull/ocm-test-suite/nextcloud.sh | 3 --- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 docker/pull/ocm-test-suite/nextcloud-sm.sh diff --git a/docker/pull/ocm-test-suite/nextcloud-sm.sh b/docker/pull/ocm-test-suite/nextcloud-sm.sh new file mode 100644 index 00000000..62004a0c --- /dev/null +++ b/docker/pull/ocm-test-suite/nextcloud-sm.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# @michielbdejong halt on error in docker init scripts. +set -e + +EFSS_PLATFORM_VERSION=${1:-"v27.1.11"} + +# 3rd party images. +docker pull mariadb:11.4.2 +docker pull cypress/included:13.13.1 + +# dev-stock images. +docker pull "pondersource/nextcloud:${EFSS_PLATFORM_VERSION}" diff --git a/docker/pull/ocm-test-suite/nextcloud.sh b/docker/pull/ocm-test-suite/nextcloud.sh index 90c8c51e..62004a0c 100755 --- a/docker/pull/ocm-test-suite/nextcloud.sh +++ b/docker/pull/ocm-test-suite/nextcloud.sh @@ -3,9 +3,6 @@ # @michielbdejong halt on error in docker init scripts. set -e -# nextcloud version: -# - v27.1.11 -# - v28.0.14 EFSS_PLATFORM_VERSION=${1:-"v27.1.11"} # 3rd party images. From 9b30277256d782986a2bb81eb0a845210cf411eb Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 15:49:56 +0330 Subject: [PATCH 160/184] update: setup fucntion for new file name schemas like nextcloud-sm-owncloud.sh --- scripts/utils/setup.sh | 35 ++++++++++++++++++++++++++++++---- scripts/utils/test_modes/ci.sh | 2 +- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/scripts/utils/setup.sh b/scripts/utils/setup.sh index c849cb10..b2df5a36 100644 --- a/scripts/utils/setup.sh +++ b/scripts/utils/setup.sh @@ -18,10 +18,37 @@ extract_platform_variables() { return fi - # For other scenarios, split the filename by '-' to get both platforms - local platform1 platform2 - IFS='-' read -r platform1 platform2 <<< "${filename}" - + # For other scenarios, split the filename at the correct hyphen + # First remove the .sh extension + local name_without_extension="${filename%.sh}" + + # Check if there's at least one hyphen + if [[ "${name_without_extension}" != *-* ]]; then + error_exit "Invalid filename format: ${filename}. Expected format: platform1-platform2.sh" + fi + + # For cases like nextcloud-sm-nextcloud-sm.sh, we need to split at the middle + # For cases like nextcloud-sm-owncloud.sh, we need to split after nextcloud-sm + # We can do this by counting from the right and splitting at the correct position + if [[ "${name_without_extension}" == *-*-*-* ]]; then + # Case with 3 hyphens (like nextcloud-sm-nextcloud-sm) + # Split at the second hyphen + local platform1=$(echo "${name_without_extension}" | cut -d'-' -f1-2) + local platform2=$(echo "${name_without_extension}" | cut -d'-' -f3-) + else + # Case with 1 or 2 hyphens + # If it matches known pattern with platform1 containing a hyphen, + # split at the second hyphen, otherwise split at the first + if [[ "${name_without_extension}" == nextcloud-sm-* ]] || + [[ "${name_without_extension}" == owncloud-sm-* ]]; then + local platform1=$(echo "${name_without_extension}" | cut -d'-' -f1-2) + local platform2=$(echo "${name_without_extension}" | cut -d'-' -f3) + else + local platform1=$(echo "${name_without_extension}" | cut -d'-' -f1) + local platform2=$(echo "${name_without_extension}" | cut -d'-' -f2-) + fi + fi + # Export the variables so the rest of the script can use them export TEST_SCENARIO="${test_scenario}" export EFSS_PLATFORM_1="${platform1}" diff --git a/scripts/utils/test_modes/ci.sh b/scripts/utils/test_modes/ci.sh index 17955bdc..18c5df85 100644 --- a/scripts/utils/test_modes/ci.sh +++ b/scripts/utils/test_modes/ci.sh @@ -12,7 +12,7 @@ run_ci() { fi else if [[ -z "${EFSS_PLATFORM_1}" || -z "${EFSS_PLATFORM_2}" ]]; then - error_exit "Usage for share: ci " + error_exit "Usage for test script: -.sh ci " fi fi From d1e2836e376ebc58ef3358080177a3f35a203a26 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 28 Jan 2025 12:21:19 +0000 Subject: [PATCH 161/184] add: +x flag on files --- dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh | 0 docker/pull/ocm-test-suite/nextcloud-sm.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh mode change 100644 => 100755 docker/pull/ocm-test-suite/nextcloud-sm.sh diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh old mode 100644 new mode 100755 diff --git a/docker/pull/ocm-test-suite/nextcloud-sm.sh b/docker/pull/ocm-test-suite/nextcloud-sm.sh old mode 100644 new mode 100755 From b7188352a03b3f14ca32b0fb280eaeef3a41a937 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 16:50:17 +0330 Subject: [PATCH 162/184] refactor: move legacy things out to their own folder, resolve em later --- .../{ => legacy}/nextcloud-solid.Dockerfile | 0 .../{ => legacy}/nextcloud-sunet.Dockerfile | 0 .../owncloud-federatedgroups.Dockerfile | 0 .../owncloud-ocm-test-suite.Dockerfile | 0 .../owncloud-opencloudmesh.Dockerfile | 0 .../owncloud-sciencemesh.Dockerfile | 0 .../owncloud-surf-trashbin.Dockerfile | 0 .../owncloud-token-based-access.Dockerfile | 0 .../nextcloud-sciencemesh.Dockerfile | 32 ------------------- .../{ => legacy}/nextcloud-ocm-test-suite.sh | 0 .../init/{ => legacy}/nextcloud-solid.sh | 0 .../init/{ => legacy}/nextcloud-sunet.sh | 0 .../{ => legacy}/owncloud-federatedgroups.sh | 0 .../{ => legacy}/owncloud-opencloudmesh.sh | 0 .../init/{ => legacy}/owncloud-sm-ocm.sh | 0 .../init/{ => legacy}/owncloud-sm-sram-ocm.sh | 0 .../{ => legacy}/owncloud-surf-trashbin.sh | 0 .../owncloud-token-based-access.sh | 0 18 files changed, 32 deletions(-) rename docker/dockerfiles/{ => legacy}/nextcloud-solid.Dockerfile (100%) rename docker/dockerfiles/{ => legacy}/nextcloud-sunet.Dockerfile (100%) rename docker/dockerfiles/{ => legacy}/owncloud-federatedgroups.Dockerfile (100%) rename docker/dockerfiles/{ => legacy}/owncloud-ocm-test-suite.Dockerfile (100%) rename docker/dockerfiles/{ => legacy}/owncloud-opencloudmesh.Dockerfile (100%) rename docker/dockerfiles/{ => legacy}/owncloud-sciencemesh.Dockerfile (100%) rename docker/dockerfiles/{ => legacy}/owncloud-surf-trashbin.Dockerfile (100%) rename docker/dockerfiles/{ => legacy}/owncloud-token-based-access.Dockerfile (100%) delete mode 100644 docker/dockerfiles/nextcloud-sciencemesh.Dockerfile rename docker/scripts/init/{ => legacy}/nextcloud-ocm-test-suite.sh (100%) mode change 100755 => 100644 rename docker/scripts/init/{ => legacy}/nextcloud-solid.sh (100%) mode change 100755 => 100644 rename docker/scripts/init/{ => legacy}/nextcloud-sunet.sh (100%) mode change 100755 => 100644 rename docker/scripts/init/{ => legacy}/owncloud-federatedgroups.sh (100%) mode change 100755 => 100644 rename docker/scripts/init/{ => legacy}/owncloud-opencloudmesh.sh (100%) mode change 100755 => 100644 rename docker/scripts/init/{ => legacy}/owncloud-sm-ocm.sh (100%) mode change 100755 => 100644 rename docker/scripts/init/{ => legacy}/owncloud-sm-sram-ocm.sh (100%) mode change 100755 => 100644 rename docker/scripts/init/{ => legacy}/owncloud-surf-trashbin.sh (100%) mode change 100755 => 100644 rename docker/scripts/init/{ => legacy}/owncloud-token-based-access.sh (100%) mode change 100755 => 100644 diff --git a/docker/dockerfiles/nextcloud-solid.Dockerfile b/docker/dockerfiles/legacy/nextcloud-solid.Dockerfile similarity index 100% rename from docker/dockerfiles/nextcloud-solid.Dockerfile rename to docker/dockerfiles/legacy/nextcloud-solid.Dockerfile diff --git a/docker/dockerfiles/nextcloud-sunet.Dockerfile b/docker/dockerfiles/legacy/nextcloud-sunet.Dockerfile similarity index 100% rename from docker/dockerfiles/nextcloud-sunet.Dockerfile rename to docker/dockerfiles/legacy/nextcloud-sunet.Dockerfile diff --git a/docker/dockerfiles/owncloud-federatedgroups.Dockerfile b/docker/dockerfiles/legacy/owncloud-federatedgroups.Dockerfile similarity index 100% rename from docker/dockerfiles/owncloud-federatedgroups.Dockerfile rename to docker/dockerfiles/legacy/owncloud-federatedgroups.Dockerfile diff --git a/docker/dockerfiles/owncloud-ocm-test-suite.Dockerfile b/docker/dockerfiles/legacy/owncloud-ocm-test-suite.Dockerfile similarity index 100% rename from docker/dockerfiles/owncloud-ocm-test-suite.Dockerfile rename to docker/dockerfiles/legacy/owncloud-ocm-test-suite.Dockerfile diff --git a/docker/dockerfiles/owncloud-opencloudmesh.Dockerfile b/docker/dockerfiles/legacy/owncloud-opencloudmesh.Dockerfile similarity index 100% rename from docker/dockerfiles/owncloud-opencloudmesh.Dockerfile rename to docker/dockerfiles/legacy/owncloud-opencloudmesh.Dockerfile diff --git a/docker/dockerfiles/owncloud-sciencemesh.Dockerfile b/docker/dockerfiles/legacy/owncloud-sciencemesh.Dockerfile similarity index 100% rename from docker/dockerfiles/owncloud-sciencemesh.Dockerfile rename to docker/dockerfiles/legacy/owncloud-sciencemesh.Dockerfile diff --git a/docker/dockerfiles/owncloud-surf-trashbin.Dockerfile b/docker/dockerfiles/legacy/owncloud-surf-trashbin.Dockerfile similarity index 100% rename from docker/dockerfiles/owncloud-surf-trashbin.Dockerfile rename to docker/dockerfiles/legacy/owncloud-surf-trashbin.Dockerfile diff --git a/docker/dockerfiles/owncloud-token-based-access.Dockerfile b/docker/dockerfiles/legacy/owncloud-token-based-access.Dockerfile similarity index 100% rename from docker/dockerfiles/owncloud-token-based-access.Dockerfile rename to docker/dockerfiles/legacy/owncloud-token-based-access.Dockerfile diff --git a/docker/dockerfiles/nextcloud-sciencemesh.Dockerfile b/docker/dockerfiles/nextcloud-sciencemesh.Dockerfile deleted file mode 100644 index bbc95df9..00000000 --- a/docker/dockerfiles/nextcloud-sciencemesh.Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM pondersource/nextcloud:v27.1.11 - -# keys for oci taken from: -# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys -LABEL org.opencontainers.image.licenses=MIT -LABEL org.opencontainers.image.title="PonderSource Nextcloud ScienceMesh Image" -LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" -LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" - -USER www-data - -ARG REPO_SCIENCEMESH=https://github.com/sciencemesh/nc-sciencemesh -ARG BRANCH_SCIENCEMESH=nextcloud -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . -# $RANDOM returns random number each time. -ARG CACHEBUST="default" -RUN git clone \ - --depth 1 \ - --branch ${BRANCH_SCIENCEMESH} \ - ${REPO_SCIENCEMESH} \ - apps/sciencemesh - -RUN cd apps/sciencemesh && git pull -RUN cd apps/sciencemesh && make - -# this file can be overrided in docker run or docker compose.yaml. -# example: docker run --volume new-init.sh:/init.sh:ro -COPY ./scripts/init/nextcloud-sciencemesh.sh /init.sh -RUN mkdir -p data; touch data/nextcloud.log - -USER root diff --git a/docker/scripts/init/nextcloud-ocm-test-suite.sh b/docker/scripts/init/legacy/nextcloud-ocm-test-suite.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/nextcloud-ocm-test-suite.sh rename to docker/scripts/init/legacy/nextcloud-ocm-test-suite.sh diff --git a/docker/scripts/init/nextcloud-solid.sh b/docker/scripts/init/legacy/nextcloud-solid.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/nextcloud-solid.sh rename to docker/scripts/init/legacy/nextcloud-solid.sh diff --git a/docker/scripts/init/nextcloud-sunet.sh b/docker/scripts/init/legacy/nextcloud-sunet.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/nextcloud-sunet.sh rename to docker/scripts/init/legacy/nextcloud-sunet.sh diff --git a/docker/scripts/init/owncloud-federatedgroups.sh b/docker/scripts/init/legacy/owncloud-federatedgroups.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/owncloud-federatedgroups.sh rename to docker/scripts/init/legacy/owncloud-federatedgroups.sh diff --git a/docker/scripts/init/owncloud-opencloudmesh.sh b/docker/scripts/init/legacy/owncloud-opencloudmesh.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/owncloud-opencloudmesh.sh rename to docker/scripts/init/legacy/owncloud-opencloudmesh.sh diff --git a/docker/scripts/init/owncloud-sm-ocm.sh b/docker/scripts/init/legacy/owncloud-sm-ocm.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/owncloud-sm-ocm.sh rename to docker/scripts/init/legacy/owncloud-sm-ocm.sh diff --git a/docker/scripts/init/owncloud-sm-sram-ocm.sh b/docker/scripts/init/legacy/owncloud-sm-sram-ocm.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/owncloud-sm-sram-ocm.sh rename to docker/scripts/init/legacy/owncloud-sm-sram-ocm.sh diff --git a/docker/scripts/init/owncloud-surf-trashbin.sh b/docker/scripts/init/legacy/owncloud-surf-trashbin.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/owncloud-surf-trashbin.sh rename to docker/scripts/init/legacy/owncloud-surf-trashbin.sh diff --git a/docker/scripts/init/owncloud-token-based-access.sh b/docker/scripts/init/legacy/owncloud-token-based-access.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/scripts/init/owncloud-token-based-access.sh rename to docker/scripts/init/legacy/owncloud-token-based-access.sh From 37fdb318a7660830cf607a0344c28c2d08022766 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 16:51:26 +0330 Subject: [PATCH 163/184] add: owncloud apps and build script for oc-sm --- docker/build/all.sh | 63 +++++++++++++++++++- docker/dockerfiles/nextcloud-app.Dockerfile | 2 +- docker/dockerfiles/owncloud-app.Dockerfile | 60 +++++++++++++++++++ docker/scripts/init/nextcloud-sciencemesh.sh | 3 + docker/scripts/init/owncloud-sciencemesh.sh | 20 +++---- 5 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 docker/dockerfiles/owncloud-app.Dockerfile diff --git a/docker/build/all.sh b/docker/build/all.sh index 61155422..b8bd8837 100755 --- a/docker/build/all.sh +++ b/docker/build/all.sh @@ -162,7 +162,7 @@ build_docker_image() { build_nextcloud_app_image() { local app_name="${1}" local app_repo="${2}" - local app_branch="${3:-main}" + local app_branch="${3:-master}" local app_build_cmd="${4:-}" local init_script="${5:-}" local nextcloud_version="${6}" @@ -197,6 +197,56 @@ build_nextcloud_app_image() { echo } +# ----------------------------------------------------------------------------------- +# Function: build_nextcloud_app_image +# Purpose: Build a Nextcloud image with a specific app installed +# Arguments: +# 1. app_name - Name of the app (e.g., "sciencemesh") +# 2. app_repo - Git repository URL +# 3. app_branch - Git branch (default: "main") +# 4. app_build_cmd - Build command if required (optional) +# 5. init_script - Path to initialization script (optional) +# 6. nextcloud_version - Nextcloud version to use as base +# 7. image_tag_suffix - Suffix for the image tag (optional) +# ----------------------------------------------------------------------------------- +build_owncloud_app_image() { + local app_name="${1}" + local app_repo="${2}" + local app_branch="${3:-master}" + local app_build_cmd="${4:-}" + local init_script="${5:-}" + local owncloud_version="${6}" + local image_tag_suffix="${7:-${app_name}}" + + local build_args="" + + # Construct build arguments string + build_args="--build-arg OWNCLOUD_VERSION=${owncloud_version}" + build_args="${build_args} --build-arg APP_NAME=${app_name}" + build_args="${build_args} --build-arg APP_REPO=${app_repo}" + build_args="${build_args} --build-arg APP_BRANCH=${app_branch}" + + # Add optional build arguments if provided + [[ -n "${app_build_cmd}" ]] && build_args="${build_args} --build-arg APP_BUILD_CMD=${app_build_cmd}" + [[ -n "${init_script}" ]] && build_args="${build_args} --build-arg INIT_SCRIPT=${init_script}" + + # Construct the image tag + local image_tag="${owncloud_version}-${image_tag_suffix}" + + echo "Building ownCloud app image: ${app_name} (${image_tag})" + if ! docker build \ + ${build_args} \ + --file "./dockerfiles/owncloud-app.Dockerfile" \ + --tag "pondersource/owncloud:${image_tag}" \ + .; then + print_error "Failed to build ownCloud app image: ${app_name}" + return 1 + fi + + echo "Successfully built ownCloud app image: ${app_name}" + echo +} + # ----------------------------------------------------------------------------------- # Main Execution # ----------------------------------------------------------------------------------- @@ -291,6 +341,17 @@ main() { "--build-arg OWNCLOUD_BRANCH=${version}" done + # Build ownCloud App Variants + # ScienceMesh + build_owncloud_app_image \ + "sciencemesh" \ + "https://github.com/sciencemesh/nc-sciencemesh" \ + "owncloud" \ + "make" \ + "./scripts/init/owncloud-sciencemesh.sh" \ + "v10.15.0" \ + "sm" + echo "All builds attempted." echo "Check the above output for any build failures or errors." } diff --git a/docker/dockerfiles/nextcloud-app.Dockerfile b/docker/dockerfiles/nextcloud-app.Dockerfile index 6a4a9ab9..dcc35ef1 100644 --- a/docker/dockerfiles/nextcloud-app.Dockerfile +++ b/docker/dockerfiles/nextcloud-app.Dockerfile @@ -4,7 +4,7 @@ FROM pondersource/nextcloud:${NEXTCLOUD_VERSION} # App installation arguments ARG APP_NAME ARG APP_REPO -ARG APP_BRANCH=main +ARG APP_BRANCH=master ARG APP_BUILD_CMD="" ARG APP_SOURCE_DIR="/ponder/apps" ARG INIT_SCRIPT="" diff --git a/docker/dockerfiles/owncloud-app.Dockerfile b/docker/dockerfiles/owncloud-app.Dockerfile new file mode 100644 index 00000000..4772025f --- /dev/null +++ b/docker/dockerfiles/owncloud-app.Dockerfile @@ -0,0 +1,60 @@ +ARG OWNCLOUD_VERSION=latest +FROM pondersource/owncloud:${OWNCLOUD_VERSION} + +# App installation arguments +ARG APP_NAME +ARG APP_REPO +ARG APP_BRANCH=master +ARG APP_BUILD_CMD="" +ARG APP_SOURCE_DIR="/ponder/apps" +ARG INIT_SCRIPT="" + +# keys for oci taken from: +# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys +LABEL org.opencontainers.image.licenses=MIT +LABEL org.opencontainers.image.title="PonderSource ownCloud with ${APP_NAME}" +LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" +LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" +LABEL org.opencontainers.image.description="ownCloud image with ${APP_NAME} pre-installed" + +USER root + +RUN set -ex; \ + \ + apt-get update; \ + apt-get install --no-install-recommends --assume-yes \ + git + +RUN mkdir -p ${APP_SOURCE_DIR}; \ + chown -R www-data:root ${APP_SOURCE_DIR}; \ + chmod -R g=u ${APP_SOURCE_DIR} + +USER www-data + +# Install the app +RUN set -ex; \ + if [ -z "${APP_NAME}" ] || [ -z "${APP_REPO}" ]; then \ + echo "Error: APP_NAME and APP_REPO must be provided"; \ + exit 1; \ + fi; \ + # Clone the app repository + git clone \ + --depth 1 \ + --branch ${APP_BRANCH} \ + ${APP_REPO} \ + ${APP_SOURCE_DIR}/${APP_NAME}; \ + # Update to latest commit + cd ${APP_SOURCE_DIR}/${APP_NAME} && git pull; \ + # Build if build command is provided + if [ -n "${APP_BUILD_CMD}" ]; then \ + ${APP_BUILD_CMD}; \ + fi + +USER root + +# After cloning, `git` is no longer needed at runtime, so remove it to reduce image size. +RUN apt-get purge -y git && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Copy init script if provided +COPY ${INIT_SCRIPT} "/docker-entrypoint-hooks.d/before-starting/${APP_NAME}.sh" +RUN chmod +x /docker-entrypoint-hooks.d/before-starting/${APP_NAME}.sh diff --git a/docker/scripts/init/nextcloud-sciencemesh.sh b/docker/scripts/init/nextcloud-sciencemesh.sh index 63c81994..02793ef4 100755 --- a/docker/scripts/init/nextcloud-sciencemesh.sh +++ b/docker/scripts/init/nextcloud-sciencemesh.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# @michielbdejong halt on error in docker init scripts +set -e + # Target paths APP_SOURCE="/ponder/apps/sciencemesh" APP_TARGET="/var/www/html/apps/sciencemesh" diff --git a/docker/scripts/init/owncloud-sciencemesh.sh b/docker/scripts/init/owncloud-sciencemesh.sh index 056494b7..9fc48aee 100755 --- a/docker/scripts/init/owncloud-sciencemesh.sh +++ b/docker/scripts/init/owncloud-sciencemesh.sh @@ -3,17 +3,17 @@ # @michielbdejong halt on error in docker init scripts set -e -php console.php maintenance:install --admin-user "${USER}" --admin-pass "${PASS}" --database "mysql" \ - --database-name "efss" --database-user "root" --database-host "${DBHOST}" \ - --database-pass "eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek" -php console.php app:disable firstrunwizard +# Target paths +APP_SOURCE="/ponder/apps/sciencemesh" +APP_TARGET="/var/www/html/apps/sciencemesh" -# change/add lines in config.php -sed -i "8 i\ 1 => 'oc1.docker'," /var/www/html/config/config.php -sed -i "9 i\ 2 => 'oc2.docker'," /var/www/html/config/config.php -sed -i "10 i\ 3 => 'owncloud1.docker'," /var/www/html/config/config.php -sed -i "11 i\ 4 => 'owncloud2.docker'," /var/www/html/config/config.php -sed -i "12 i\ 5 => (isset(\$_SERVER['HTTP_HOST']) ? \$_SERVER['HTTP_HOST'] : 'localhost')," /var/www/html/config/config.php +# Remove existing directory or symlink if it exists +if [ -e "${APP_TARGET}" ] || [ -L "${APP_TARGET}" ]; then + rm -rf "${APP_TARGET}" +fi + +# Create new symlink +ln -sf "${APP_SOURCE}" "${APP_TARGET}" php console.php app:enable sciencemesh From 84cbd089ec37201d57286cd524654daab886c31f Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 17:05:11 +0330 Subject: [PATCH 164/184] add: oc-sm to oc-sm invite link share test --- ...ml => invite-link-oc-sm-v10-oc-sm-v10.yml} | 10 +- ... owncloud-sm-v10-to-owncloud-sm-v10.cy.js} | 0 .../invite-link/nextcloud-sm-nextcloud-sm.sh | 5 - .../invite-link/owncloud-owncloud.sh | 321 ------------------ .../invite-link/owncloud-sm-owncloud-sm.sh | 147 ++++++++ 5 files changed, 152 insertions(+), 331 deletions(-) rename .github/workflows/{invite-link-oc-v10-oc-v10.yml => invite-link-oc-sm-v10-oc-sm-v10.yml} (90%) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{owncloud-v10-to-owncloud-v10.cy.js => owncloud-sm-v10-to-owncloud-sm-v10.cy.js} (100%) delete mode 100755 dev/ocm-test-suite/invite-link/owncloud-owncloud.sh create mode 100644 dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh diff --git a/.github/workflows/invite-link-oc-v10-oc-v10.yml b/.github/workflows/invite-link-oc-sm-v10-oc-sm-v10.yml similarity index 90% rename from .github/workflows/invite-link-oc-v10-oc-v10.yml rename to .github/workflows/invite-link-oc-sm-v10-oc-sm-v10.yml index ec5d39ca..d5fdcffa 100644 --- a/.github/workflows/invite-link-oc-v10-oc-v10.yml +++ b/.github/workflows/invite-link-oc-sm-v10-oc-sm-v10.yml @@ -1,4 +1,4 @@ -name: OCM Test Invite Link OC v10.15.0 to OC v10.15.0 +name: OCM Test Invite Link OC SM v10.15.0 to OC SM v10.15.0 # Controls when the action will run. on: @@ -28,14 +28,14 @@ jobs: matrix: sender: [ { - platform: owncloud, - version: v10.15.0 + platform: owncloud-sm, + version: v10.15.0-sm }, ] receiver: [ { - platform: owncloud, - version: v10.15.0 + platform: owncloud-sm, + version: v10.15.0-sm }, ] diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-owncloud-v10.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh index 8e0f8519..1bd9aa8c 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh @@ -20,11 +20,6 @@ # SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. # BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. -# Requirements: -# - Docker and required images must be installed. -# - Test scripts and configurations must be located in the expected directories. -# - Ensure that the necessary scripts (e.g., init scripts) and configurations exist. - # Example: # ./nextcloud-nextcloud.sh v28.0.12 v27.1.11 ci electron diff --git a/dev/ocm-test-suite/invite-link/owncloud-owncloud.sh b/dev/ocm-test-suite/invite-link/owncloud-owncloud.sh deleted file mode 100755 index 2d57b491..00000000 --- a/dev/ocm-test-suite/invite-link/owncloud-owncloud.sh +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_2_VERSION=${2:-"v10.15.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} - -function createReva() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "creating reva for ${platform} ${number}" - - # make sure scripts are executable. - chmod +x "${ENV_ROOT}/temp/reva/run.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/kill.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/entrypoint.sh" >/dev/null 2>&1 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/temp/reva/run.sh:/usr/bin/run.sh" \ - -v "${ENV_ROOT}/temp/reva/kill.sh:/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/temp/reva/entrypoint.sh:/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad -} - -function sciencemeshInsertIntoDB() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "configuring ScienceMesh app for efss ${platform} ${number}" - - # run db injections. - mysql_cmd="docker exec "maria${platform}${number}.docker" mariadb -u root -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek efss" - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/configs" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/temp/" -cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/temp/reva/configs" -cp -f "${ENV_ROOT}/docker/scripts/ocmstub/index.js" "${ENV_ROOT}/temp/index.js" -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sciencemesh.sh" "${ENV_ROOT}/temp/owncloud.sh" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-sciencemesh.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# remove unnecessary configs. -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-codimd.toml" -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-collabora.toml" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -################ -### ownCloud ### -################ - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownClouds. -createEfss owncloud 1 marie radioactivity owncloud.sh latest sciencemesh -createEfss owncloud 2 mahdi baghbani owncloud.sh latest sciencemesh - -############ -### Reva ### -############ - -# syntax: -# createReva platform number port. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# port: maps a port on local host to port 80 of reva, for `curl` purposes! should be unique. -# for all createReva commands, if the port is not unique or is already in use by another. -# program, script would halt! - -createReva owncloud 1 -createReva owncloud 2 - -################### -### ScienceMesh ### -################### - -# syntax: -# sciencemeshInsertIntoDB platform number. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. - -sciencemeshInsertIntoDB owncloud 1 -sciencemeshInsertIntoDB owncloud 2 - -###################### -### Mesh directory ### -###################### -docker run --detach --network=testnet \ - --name=meshdir.docker \ - -e HOST="meshdir" \ - -v "${ENV_ROOT}/temp/index.js:/ocmstub/index.js" \ - pondersource/dev-stock-ocmstub \ - >/dev/null 2>&1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://owncloud1.docker -> username: marie password: radioactivity" - echo "https://owncloud2.docker -> username: mahdi password: baghbani" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/owncloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi diff --git a/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh b/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh new file mode 100644 index 00000000..0038e1f4 --- /dev/null +++ b/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test ownCloud to ownCloud OCM invite link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as ownCloud, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. + +# Usage: +# ./owncloud-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] + +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the primary EFSS platform (default: "v10.15.0"). +# EFSS_PLATFORM_2_VERSION : Version of the secondary EFSS platform (default: "v10.15.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. + +# Example: +# ./owncloud-owncloud.sh v10.15.0 v10.15.0 ci electron + +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v10.15.0-sm" +DEFAULT_EFSS_2_VERSION="v10.15.0-sm" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 2 "mahdi" "baghbani" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + # Create Reva containers with disabled app configs + local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" + create_reva "owncloud" 1 pondersource/revad latest "${disabled_configs}" + create_reva "owncloud" 2 pondersource/revad latest "${disabled_configs}" + + # Configure ScienceMesh integration + configure_sciencemesh "owncloud" 1 "https://revaowncloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + configure_sciencemesh "owncloud" 2 "https://revaowncloud2.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + + # Start Mesh Directory + create_meshdir pondersource/ocmstub v1.0.0 + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://owncloud1.docker (username: marie, password: radioactivity)" \ + "https://owncloud2.docker (username: mahdi, password: baghbani)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From 1e9c76a7ee99324582a66c1286e4bb80b26fa73f Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 28 Jan 2025 13:35:46 +0000 Subject: [PATCH 165/184] fix: bash file permission --- dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh diff --git a/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh b/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh old mode 100644 new mode 100755 From 9a10a731c6aec2e82b34a00586baf5234663f141 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 18:31:56 +0330 Subject: [PATCH 166/184] fix: typo --- dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh | 4 ---- dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh | 4 ---- 2 files changed, 8 deletions(-) diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh index 1bd9aa8c..f2fa470f 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh @@ -10,19 +10,15 @@ # This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms # such as Nextcloud, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. # It supports both development and CI environments, with optional browser support. - # Usage: # ./nextcloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] - # Arguments: # EFSS_PLATFORM_1_VERSION : Version of the primary EFSS platform (default: "v27.1.11"). # EFSS_PLATFORM_2_VERSION : Version of the secondary EFSS platform (default: "v27.1.11"). # SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. # BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. - # Example: # ./nextcloud-nextcloud.sh v28.0.12 v27.1.11 ci electron - # ----------------------------------------------------------------------------------- # Exit immediately if a command exits with a non-zero status, diff --git a/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh b/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh index 0038e1f4..0f783b84 100755 --- a/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh +++ b/dev/ocm-test-suite/invite-link/owncloud-sm-owncloud-sm.sh @@ -10,19 +10,15 @@ # This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms # such as ownCloud, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. # It supports both development and CI environments, with optional browser support. - # Usage: # ./owncloud-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] - # Arguments: # EFSS_PLATFORM_1_VERSION : Version of the primary EFSS platform (default: "v10.15.0"). # EFSS_PLATFORM_2_VERSION : Version of the secondary EFSS platform (default: "v10.15.0"). # SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. # BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. - # Example: # ./owncloud-owncloud.sh v10.15.0 v10.15.0 ci electron - # ----------------------------------------------------------------------------------- # Exit immediately if a command exits with a non-zero status, From 9cb7620d2ab7b66ba8bcdf11e987f8a9612d6e27 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 18:32:34 +0330 Subject: [PATCH 167/184] add: ocis configuration --- dev/ocm-test-suite/invite-link/ocis-ocis.sh | 390 ++++++++------------ scripts/utils/container/ocis.sh | 75 ++++ 2 files changed, 231 insertions(+), 234 deletions(-) diff --git a/dev/ocm-test-suite/invite-link/ocis-ocis.sh b/dev/ocm-test-suite/invite-link/ocis-ocis.sh index c58a2065..fded29ec 100755 --- a/dev/ocm-test-suite/invite-link/ocis-ocis.sh +++ b/dev/ocm-test-suite/invite-link/ocis-ocis.sh @@ -1,244 +1,166 @@ #!/usr/bin/env bash -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# oCIS version: -# - 5.0.9 -EFSS_PLATFORM_1_VERSION=${1:-"5.0.9"} - -# oCIS version: -# - 5.0.9 -EFSS_PLATFORM_2_VERSION=${2:-"5.0.9"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} +# ----------------------------------------------------------------------------------- +# Script to Test oCIS to oCIS OCM invite-link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# such as oCIS, using Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. +# Usage: +# ./ocis-ocis.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "5.0.9"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "5.0.9"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +# Example: +# ./ocis-ocis.sh 5.0.9 5.0.9 ci electron +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="5.0.9" +DEFAULT_EFSS_2_VERSION="5.0.9" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT else - "$@" + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 fi } -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function changeInFile() { - local file_path="${1}" - local original="${2}" - local replacement="${3}" - - sed -i "s#${original}#${replacement}#g" "${file_path}" -} - -function createEfssOcis() { - local number="${1}" - - redirect_to_null_cmd echo "creating efss ocis ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="ocis${number}.docker" \ - -e OCIS_LOG_LEVEL=info \ - -e OCIS_LOG_COLOR=true \ - -e OCIS_LOG_PRETTY=true \ - -e PROXY_HTTP_ADDR=0.0.0.0:443 \ - -e OCIS_URL="https://ocis${number}.docker" \ - -e OCIS_INSECURE=true \ - -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" \ - -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" \ - -e PROXY_ENABLE_BASIC_AUTH=true \ - -e IDM_ADMIN_PASSWORD=admin \ - -e IDM_CREATE_DEMO_USERS=true \ - -e FRONTEND_OCS_INCLUDE_OCM_SHAREES=true \ - -e FRONTEND_OCS_LIST_OCM_SHARES=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING=true \ - -e OCIS_ADD_RUN_SERVICES=ocm \ - -e OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE=/dev-stock/ocmproviders.json \ - -e GRAPH_INCLUDE_OCM_SHAREES=true \ - -e OCM_OCM_INVITE_MANAGER_INSECURE=true \ - -e OCM_OCM_SHARE_PROVIDER_INSECURE=true \ - -e OCM_OCM_STORAGE_PROVIDER_INSECURE=true \ - -e WEB_UI_CONFIG_FILE=/dev-stock/web-ui-config.json \ - -v "${ENV_ROOT}/temp/ocis:/dev-stock" \ - -v "${ENV_ROOT}/temp/certificates:/certificates" \ - -v "${ENV_ROOT}/temp/certificate-authority:/certificate-authority" \ - --entrypoint /bin/sh \ - "owncloud/ocis:5.0.9@sha256:96671605863b38b0b8021400fdb2d843586dfa31451a8c7766f15eabe85d8267" \ - -c "ocis init || true; ocis server" -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/certificates" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/configs/ocis" "${ENV_ROOT}/temp/ocis" -cp -f "${ENV_ROOT}/docker/tls/certificates/ocis"* "${ENV_ROOT}/temp/certificates" -cp -fr "${ENV_ROOT}/docker/tls/certificate-authority" "${ENV_ROOT}/temp/certificate-authority" - -# fix permissions. -chmod -R 777 "${ENV_ROOT}/temp/certificates" -chmod -R 777 "${ENV_ROOT}/temp/certificate-authority" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -# insert real domain names into ocmproviders.json -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--domain--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--homepage--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--path--|" "ocis1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--host--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--path--|" "ocis1.docker/dav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--host--|" "ocis1.docker" - -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--domain--|" "ocis2.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--homepage--|" "ocis2.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--|" "ocis2.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--path--|" "ocis2.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--host--|" "ocis2.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--|" "ocis2.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--path--|" "ocis2.docker/dav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "ocis2.docker" - -############ -### oCIS ### -############ - -# syntax: -# createEfssOcis number. +# ----------------------------------------------------------------------------------- +# Main Execution +# Purpose : +# 1) Initialize the environment +# 2) Parse CLI arguments and validate necessary files +# 3) Prepare environment (clean up, create Docker network, etc.) +# 4) Create EFSS containers +# 5) Run dev or CI mode depending on SCRIPT_MODE # +# Arguments: +# All command line arguments are passed to parse_arguments. # -# number: should be unique for each oCIS, for example: you cannot have two oCIS with same number. - -# oCISes. -createEfssOcis 1 -createEfssOcis 2 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://ocis1.docker -> username: einstein password: relativity" - echo "https://ocis2.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/ocis-${P1_VER}-to-ocis-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi +# Returns : None - the script will exit upon errors (via error_exit) or complete normally. +# ----------------------------------------------------------------------------------- +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Prepare oCIS specific environment with appropriate configurations + # case "${TEST_SCENARIO}" in + # "ocis-ocis") + # prepare_ocis_environment \ + # "ocis1.docker,ocis1.docker,dav/" \ + # "ocis2.docker,ocis2.docker,dav/" + # ;; + # "ocis-owncloud") + # prepare_ocis_environment \ + # "ocis1.docker,ocis1.docker,dav/" \ + # "revaowncloud1.docker,owncloud1.docker,remote.php/webdav/" + # ;; + # "ocis-nextcloud") + # prepare_ocis_environment \ + # "ocis1.docker,ocis1.docker,dav/" \ + # "revanextcloud1.docker,nextcloud1.docker,remote.php/webdav/" + # ;; + # *) + # error_exit "Unknown test scenario: ${TEST_SCENARIO}" + # ;; + # esac + + prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "ocis2.docker,ocis2.docker,dav/" + + # Create EFSS containers + # # id # image # tag + create_ocis 1 "owncloud/ocis" "${EFSS_PLATFORM_1_VERSION}" + create_ocis 2 "owncloud/ocis" "${EFSS_PLATFORM_2_VERSION}" + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://ocis1.docker (username: einstein, password: relativity)" \ + "https://ocis2.docker (username: marie, password: radioactivity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh index 4a948e73..4b76ab99 100644 --- a/scripts/utils/container/ocis.sh +++ b/scripts/utils/container/ocis.sh @@ -22,6 +22,18 @@ create_ocis() { -e PROXY_ENABLE_BASIC_AUTH=true \ -e IDM_ADMIN_PASSWORD=admin \ -e IDM_CREATE_DEMO_USERS=true \ + -e FRONTEND_OCS_INCLUDE_OCM_SHAREES=true \ + -e FRONTEND_OCS_LIST_OCM_SHARES=true \ + -e FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING=true \ + -e FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING=true \ + -e OCIS_ADD_RUN_SERVICES=ocm \ + -e OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE=/dev-stock/ocmproviders.json \ + -e GRAPH_INCLUDE_OCM_SHAREES=true \ + -e OCM_OCM_INVITE_MANAGER_INSECURE=true \ + -e OCM_OCM_SHARE_PROVIDER_INSECURE=true \ + -e OCM_OCM_STORAGE_PROVIDER_INSECURE=true \ + -e WEB_UI_CONFIG_FILE=/dev-stock/web-ui-config.json \ + -v "${TEMP_DIR}/ocis:/dev-stock" \ -v "${TLS_CERT_DIR}:/certificates" \ -v "${TLS_CA_DIR}:/certificate-authority" \ --entrypoint /bin/sh \ @@ -32,3 +44,66 @@ create_ocis() { # TODO @MahdiBaghbani: we might need custom images with ss installed. # run_quietly_if_ci wait_for_port "ocis${number}.docker" 443 } + +# ----------------------------------------------------------------------------------- +# Function: configure_ocm_providers +# Purpose : Configure OCM providers for oCIS instances +# Arguments: +# $1 - First instance configuration (e.g., "ocis1.docker,ocis1.docker,dav/") +# $2 - Second instance configuration (e.g., "revanextcloud1.docker,nextcloud1.docker,remote.php/webdav/") +# +# Format for each instance configuration: +# "ocm_domain,webdav_domain,webdav_path" +# where: +# - ocm_domain: domain for OCM-related endpoints +# - webdav_domain: domain for WebDAV endpoints +# - webdav_path: path for WebDAV endpoints +# ----------------------------------------------------------------------------------- +configure_ocm_providers() { + local instance1_config="${1:-ocis1.docker,ocis1.docker,dav/}" + local instance2_config="${2:-ocis2.docker,ocis2.docker,dav/}" + + # Parse instance1 configuration + IFS=',' read -r ocm1_domain webdav1_domain webdav1_path <<< "${instance1_config}" + + # Parse instance2 configuration + IFS=',' read -r ocm2_domain webdav2_domain webdav2_path <<< "${instance2_config}" + + # Configure instance1 + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--domain--|" "${ocm1_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--homepage--|" "${ocm1_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--|" "${ocm1_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--path--|" "${ocm1_domain}/ocm/" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--host--|" "${ocm1_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--|" "${webdav1_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--path--|" "${webdav1_domain}/${webdav1_path}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--host--|" "${webdav1_domain}" + + # Configure instance2 + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--domain--|" "${ocm2_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--homepage--|" "${ocm2_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--|" "${ocm2_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--path--|" "${ocm2_domain}/ocm/" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--host--|" "${ocm2_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--|" "${webdav2_domain}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--path--|" "${webdav2_domain}/${webdav2_path}" + changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "${webdav2_domain}" +} + +# ----------------------------------------------------------------------------------- +# Function: prepare_ocis_environment +# Purpose : Prepare the environment for oCIS instances +# Arguments: +# $1 - First instance configuration (optional) +# $2 - Second instance configuration (optional) +# ----------------------------------------------------------------------------------- +prepare_ocis_environment() { + local instance1_config="${1:-}" + local instance2_config="${2:-}" + + # copy init files. + cp -fr "${ENV_ROOT}/docker/configs/ocis" "${TEMP_DIR}/ocis" + + # Configure OCM providers + configure_ocm_providers "${instance1_config}" "${instance2_config}" +} From 332912e047d64ab03761326301915b4bca8fe40f Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 28 Jan 2025 15:03:33 +0000 Subject: [PATCH 168/184] add: +x flag for bash scripts --- scripts/utils/constants.sh | 0 scripts/utils/container/cypress.sh | 0 scripts/utils/container/firefox.sh | 0 scripts/utils/container/mehsdir.sh | 0 scripts/utils/container/nextcloud.sh | 0 scripts/utils/container/ocis.sh | 0 scripts/utils/container/ocmstub.sh | 0 scripts/utils/container/owncloud.sh | 0 scripts/utils/container/reva.sh | 0 scripts/utils/container/sciencemesh.sh | 0 scripts/utils/container/seafile.sh | 0 scripts/utils/container/vnc.sh | 0 scripts/utils/docker.sh | 0 scripts/utils/environment.sh | 0 scripts/utils/errors.sh | 0 scripts/utils/setup.sh | 0 scripts/utils/test_modes/ci.sh | 0 scripts/utils/test_modes/dev.sh | 0 scripts/utils/validation.sh | 0 19 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/utils/constants.sh mode change 100644 => 100755 scripts/utils/container/cypress.sh mode change 100644 => 100755 scripts/utils/container/firefox.sh mode change 100644 => 100755 scripts/utils/container/mehsdir.sh mode change 100644 => 100755 scripts/utils/container/nextcloud.sh mode change 100644 => 100755 scripts/utils/container/ocis.sh mode change 100644 => 100755 scripts/utils/container/ocmstub.sh mode change 100644 => 100755 scripts/utils/container/owncloud.sh mode change 100644 => 100755 scripts/utils/container/reva.sh mode change 100644 => 100755 scripts/utils/container/sciencemesh.sh mode change 100644 => 100755 scripts/utils/container/seafile.sh mode change 100644 => 100755 scripts/utils/container/vnc.sh mode change 100644 => 100755 scripts/utils/docker.sh mode change 100644 => 100755 scripts/utils/environment.sh mode change 100644 => 100755 scripts/utils/errors.sh mode change 100644 => 100755 scripts/utils/setup.sh mode change 100644 => 100755 scripts/utils/test_modes/ci.sh mode change 100644 => 100755 scripts/utils/test_modes/dev.sh mode change 100644 => 100755 scripts/utils/validation.sh diff --git a/scripts/utils/constants.sh b/scripts/utils/constants.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/cypress.sh b/scripts/utils/container/cypress.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/firefox.sh b/scripts/utils/container/firefox.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/mehsdir.sh b/scripts/utils/container/mehsdir.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/nextcloud.sh b/scripts/utils/container/nextcloud.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/ocmstub.sh b/scripts/utils/container/ocmstub.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/owncloud.sh b/scripts/utils/container/owncloud.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/reva.sh b/scripts/utils/container/reva.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/sciencemesh.sh b/scripts/utils/container/sciencemesh.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/seafile.sh b/scripts/utils/container/seafile.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/container/vnc.sh b/scripts/utils/container/vnc.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/docker.sh b/scripts/utils/docker.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/environment.sh b/scripts/utils/environment.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/errors.sh b/scripts/utils/errors.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/setup.sh b/scripts/utils/setup.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/test_modes/ci.sh b/scripts/utils/test_modes/ci.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/test_modes/dev.sh b/scripts/utils/test_modes/dev.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/validation.sh b/scripts/utils/validation.sh old mode 100644 new mode 100755 From bf4fd450324452ebd540914927fc9404ff713f80 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 18:36:04 +0330 Subject: [PATCH 169/184] fix: missing function --- scripts/utils/container/ocis.sh | 38 ++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh index 4b76ab99..a2c44299 100755 --- a/scripts/utils/container/ocis.sh +++ b/scripts/utils/container/ocis.sh @@ -45,6 +45,24 @@ create_ocis() { # run_quietly_if_ci wait_for_port "ocis${number}.docker" 443 } +# ----------------------------------------------------------------------------------- +# Function: prepare_ocis_environment +# Purpose : Prepare the environment for oCIS instances +# Arguments: +# $1 - First instance configuration (optional) +# $2 - Second instance configuration (optional) +# ----------------------------------------------------------------------------------- +prepare_ocis_environment() { + local instance1_config="${1:-}" + local instance2_config="${2:-}" + + # copy init files. + cp -fr "${ENV_ROOT}/docker/configs/ocis" "${TEMP_DIR}/ocis" + + # Configure OCM providers + configure_ocm_providers "${instance1_config}" "${instance2_config}" +} + # ----------------------------------------------------------------------------------- # Function: configure_ocm_providers # Purpose : Configure OCM providers for oCIS instances @@ -90,20 +108,10 @@ configure_ocm_providers() { changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "${webdav2_domain}" } -# ----------------------------------------------------------------------------------- -# Function: prepare_ocis_environment -# Purpose : Prepare the environment for oCIS instances -# Arguments: -# $1 - First instance configuration (optional) -# $2 - Second instance configuration (optional) -# ----------------------------------------------------------------------------------- -prepare_ocis_environment() { - local instance1_config="${1:-}" - local instance2_config="${2:-}" - - # copy init files. - cp -fr "${ENV_ROOT}/docker/configs/ocis" "${TEMP_DIR}/ocis" +function changeInFile() { + local file_path="${1}" + local original="${2}" + local replacement="${3}" - # Configure OCM providers - configure_ocm_providers "${instance1_config}" "${instance2_config}" + sed -i "s#${original}#${replacement}#g" "${file_path}" } From ab61c7eb436b0ef079d42d1541fad9a413b1a668 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 18:39:27 +0330 Subject: [PATCH 170/184] fix: path of ocmprovider.json --- scripts/utils/container/ocis.sh | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/utils/container/ocis.sh b/scripts/utils/container/ocis.sh index a2c44299..2fdf2dcb 100755 --- a/scripts/utils/container/ocis.sh +++ b/scripts/utils/container/ocis.sh @@ -88,24 +88,24 @@ configure_ocm_providers() { IFS=',' read -r ocm2_domain webdav2_domain webdav2_path <<< "${instance2_config}" # Configure instance1 - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--domain--|" "${ocm1_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--homepage--|" "${ocm1_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--|" "${ocm1_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--path--|" "${ocm1_domain}/ocm/" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--host--|" "${ocm1_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--|" "${webdav1_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--path--|" "${webdav1_domain}/${webdav1_path}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--host--|" "${webdav1_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance1--domain--|" "${ocm1_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance1--homepage--|" "${ocm1_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance1--ocm--|" "${ocm1_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance1--ocm--path--|" "${ocm1_domain}/ocm/" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance1--ocm--host--|" "${ocm1_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance1--webdav--|" "${webdav1_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance1--webdav--path--|" "${webdav1_domain}/${webdav1_path}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance1--webdav--host--|" "${webdav1_domain}" # Configure instance2 - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--domain--|" "${ocm2_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--homepage--|" "${ocm2_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--|" "${ocm2_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--path--|" "${ocm2_domain}/ocm/" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--host--|" "${ocm2_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--|" "${webdav2_domain}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--path--|" "${webdav2_domain}/${webdav2_path}" - changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "${webdav2_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance2--domain--|" "${ocm2_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance2--homepage--|" "${ocm2_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance2--ocm--|" "${ocm2_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance2--ocm--path--|" "${ocm2_domain}/ocm/" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance2--ocm--host--|" "${ocm2_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance2--webdav--|" "${webdav2_domain}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance2--webdav--path--|" "${webdav2_domain}/${webdav2_path}" + changeInFile "${TEMP_DIR}/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "${webdav2_domain}" } function changeInFile() { From 707c38a12ac23c2249a2b7ccdd98d5edf83bf134 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 19:34:05 +0330 Subject: [PATCH 171/184] fix: temp folder --- scripts/utils/docker.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/utils/docker.sh b/scripts/utils/docker.sh index 7b2cdfa5..9df7bcd4 100755 --- a/scripts/utils/docker.sh +++ b/scripts/utils/docker.sh @@ -8,7 +8,8 @@ run_docker_container() { # Prepare Docker environment (network, cleanup) prepare_environment() { # Prepare temporary directories and copy necessary files - remove_directory "${ENV_ROOT}/${TEMP_DIR}" && mkdir -p "${ENV_ROOT}/${TEMP_DIR}" + remove_directory "${TEMP_DIR}" + mkdir -p "${TEMP_DIR}" # Clean up previous resources (if the cleanup script is available) if [ -x "${ENV_ROOT}/scripts/clean.sh" ]; then From f46d1de6ccbd0d28665efc9c0b8292608c004716 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 19:51:30 +0330 Subject: [PATCH 172/184] refactor: file names for the sciencemesh variants of oc and nc --- ...-oc-v10.yml => invite-link-nc-sm-v27-oc-sm-v10.yml} | 10 +++++----- ...v5-oc-v10.yml => invite-link-nc-sm-v27-ocis-v5.yml} | 10 +++++----- ...-nc-v27.yml => invite-link-oc-sm-v10-nc-sm-v27.yml} | 10 +++++----- ...v5-nc-v27.yml => invite-link-oc-sm-v10-ocis-v5.yml} | 10 +++++----- ...0-ocis-v5.yml => invite-link-ocis-v5-nc-sm-v27.yml} | 10 +++++----- ...7-ocis-v5.yml => invite-link-ocis-v5-oc-sm-v10.yml} | 10 +++++----- ...-ocis-5.cy.js => nextcloud-sm-v27-to-ocis-v5.cy.js} | 0 ...cy.js => nextcloud-sm-v27-to-owncloud-sm-v10.cy.js} | 0 ...oud-v27.cy.js => ocis-v5-to-nextcloud-sm-v27.cy.js} | 0 ...ocis-5-to-ocis-5.cy.js => ocis-v5-to-ocis-v5.cy.js} | 0 ...loud-v10.cy.js => ocis-v5-to-owncloud-sm-v10.cy.js} | 0 ...cy.js => owncloud-sm-v10-to-nextcloud-sm-v27.cy.js} | 0 ...o-ocis-5.cy.js => owncloud-sm-v10-to-ocis-v5.cy.js} | 0 13 files changed, 30 insertions(+), 30 deletions(-) rename .github/workflows/{invite-link-nc-v27-oc-v10.yml => invite-link-nc-sm-v27-oc-sm-v10.yml} (90%) rename .github/workflows/{invite-link-ocis-v5-oc-v10.yml => invite-link-nc-sm-v27-ocis-v5.yml} (94%) rename .github/workflows/{invite-link-oc-v10-nc-v27.yml => invite-link-oc-sm-v10-nc-sm-v27.yml} (90%) rename .github/workflows/{invite-link-ocis-v5-nc-v27.yml => invite-link-oc-sm-v10-ocis-v5.yml} (94%) rename .github/workflows/{invite-link-oc-v10-ocis-v5.yml => invite-link-ocis-v5-nc-sm-v27.yml} (94%) rename .github/workflows/{invite-link-nc-v27-ocis-v5.yml => invite-link-ocis-v5-oc-sm-v10.yml} (94%) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{nextcloud-v27-to-ocis-5.cy.js => nextcloud-sm-v27-to-ocis-v5.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{nextcloud-v27-to-owncloud-v10.cy.js => nextcloud-sm-v27-to-owncloud-sm-v10.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{ocis-5-to-nextcloud-v27.cy.js => ocis-v5-to-nextcloud-sm-v27.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{ocis-5-to-ocis-5.cy.js => ocis-v5-to-ocis-v5.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{ocis-5-to-owncloud-v10.cy.js => ocis-v5-to-owncloud-sm-v10.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{owncloud-v10-to-nextcloud-v27.cy.js => owncloud-sm-v10-to-nextcloud-sm-v27.cy.js} (100%) rename cypress/ocm-test-suite/cypress/e2e/invite-link/{owncloud-v10-to-ocis-5.cy.js => owncloud-sm-v10-to-ocis-v5.cy.js} (100%) diff --git a/.github/workflows/invite-link-nc-v27-oc-v10.yml b/.github/workflows/invite-link-nc-sm-v27-oc-sm-v10.yml similarity index 90% rename from .github/workflows/invite-link-nc-v27-oc-v10.yml rename to .github/workflows/invite-link-nc-sm-v27-oc-sm-v10.yml index 8598146a..12c70947 100644 --- a/.github/workflows/invite-link-nc-v27-oc-v10.yml +++ b/.github/workflows/invite-link-nc-sm-v27-oc-sm-v10.yml @@ -1,4 +1,4 @@ -name: OCM Test Invite Link NC v27.1.11 to OC v10.15.0 +name: OCM Test Invite Link NC SM v27.1.11 to OC SM v10.15.0 # Controls when the action will run. on: @@ -28,14 +28,14 @@ jobs: matrix: sender: [ { - platform: nextcloud, - version: v27.1.11 + platform: nextcloud-sm, + version: v27.1.11-sm }, ] receiver: [ { - platform: owncloud, - version: v10.15.0 + platform: owncloud-sm, + version: v10.15.0-sm }, ] diff --git a/.github/workflows/invite-link-ocis-v5-oc-v10.yml b/.github/workflows/invite-link-nc-sm-v27-ocis-v5.yml similarity index 94% rename from .github/workflows/invite-link-ocis-v5-oc-v10.yml rename to .github/workflows/invite-link-nc-sm-v27-ocis-v5.yml index e7c4a3fc..a7cf0d6d 100644 --- a/.github/workflows/invite-link-ocis-v5-oc-v10.yml +++ b/.github/workflows/invite-link-nc-sm-v27-ocis-v5.yml @@ -1,4 +1,4 @@ -name: OCM Test Invite Link oCIS v5.0.9 to OC v10.15.0 +name: OCM Test Invite Link NC SM v27.1.11 to oCIS v5.0.9 # Controls when the action will run. on: @@ -28,14 +28,14 @@ jobs: matrix: sender: [ { - platform: ocis, - version: v5.0.9 + platform: nextcloud-sm, + version: v27.1.11-sm }, ] receiver: [ { - platform: owncloud, - version: v10.15.0 + platform: ocis, + version: v5.0.9 }, ] diff --git a/.github/workflows/invite-link-oc-v10-nc-v27.yml b/.github/workflows/invite-link-oc-sm-v10-nc-sm-v27.yml similarity index 90% rename from .github/workflows/invite-link-oc-v10-nc-v27.yml rename to .github/workflows/invite-link-oc-sm-v10-nc-sm-v27.yml index 07b2dd56..dc0f6ad6 100644 --- a/.github/workflows/invite-link-oc-v10-nc-v27.yml +++ b/.github/workflows/invite-link-oc-sm-v10-nc-sm-v27.yml @@ -1,4 +1,4 @@ -name: OCM Test Invite Link OC v10.15.0 to NC v27.1.11 +name: OCM Test Invite Link OC SM v10.15.0 to NC SM v27.1.11 # Controls when the action will run. on: @@ -28,14 +28,14 @@ jobs: matrix: sender: [ { - platform: owncloud, - version: v10.15.0 + platform: owncloud-sm, + version: v10.15.0-sm }, ] receiver: [ { - platform: nextcloud, - version: v27.1.11 + platform: nextcloud-sm, + version: v27.1.11-sm }, ] diff --git a/.github/workflows/invite-link-ocis-v5-nc-v27.yml b/.github/workflows/invite-link-oc-sm-v10-ocis-v5.yml similarity index 94% rename from .github/workflows/invite-link-ocis-v5-nc-v27.yml rename to .github/workflows/invite-link-oc-sm-v10-ocis-v5.yml index acb08b29..84af1ddd 100644 --- a/.github/workflows/invite-link-ocis-v5-nc-v27.yml +++ b/.github/workflows/invite-link-oc-sm-v10-ocis-v5.yml @@ -1,4 +1,4 @@ -name: OCM Test Invite Link oCIS v5.0.9 to NC v27.1.11 +name: OCM Test Invite Link OC SM v10.15.0 to oCIS v5.0.9 # Controls when the action will run. on: @@ -28,14 +28,14 @@ jobs: matrix: sender: [ { - platform: ocis, - version: v5.0.9 + platform: owncloud-sm, + version: v10.15.0-sm }, ] receiver: [ { - platform: nextcloud, - version: v27.1.11 + platform: ocis, + version: v5.0.9 }, ] diff --git a/.github/workflows/invite-link-oc-v10-ocis-v5.yml b/.github/workflows/invite-link-ocis-v5-nc-sm-v27.yml similarity index 94% rename from .github/workflows/invite-link-oc-v10-ocis-v5.yml rename to .github/workflows/invite-link-ocis-v5-nc-sm-v27.yml index 05acdbc2..af9554ef 100644 --- a/.github/workflows/invite-link-oc-v10-ocis-v5.yml +++ b/.github/workflows/invite-link-ocis-v5-nc-sm-v27.yml @@ -1,4 +1,4 @@ -name: OCM Test Invite Link OC v10.15.0 to oCIS v5.0.9 +name: OCM Test Invite Link oCIS v5.0.9 to NC SM v27.1.11 # Controls when the action will run. on: @@ -28,14 +28,14 @@ jobs: matrix: sender: [ { - platform: owncloud, - version: v10.15.0 + platform: ocis, + version: v5.0.9 }, ] receiver: [ { - platform: ocis, - version: v5.0.9 + platform: nextcloud-sm, + version: v27.1.11-sm }, ] diff --git a/.github/workflows/invite-link-nc-v27-ocis-v5.yml b/.github/workflows/invite-link-ocis-v5-oc-sm-v10.yml similarity index 94% rename from .github/workflows/invite-link-nc-v27-ocis-v5.yml rename to .github/workflows/invite-link-ocis-v5-oc-sm-v10.yml index dcc80afd..9289430f 100644 --- a/.github/workflows/invite-link-nc-v27-ocis-v5.yml +++ b/.github/workflows/invite-link-ocis-v5-oc-sm-v10.yml @@ -1,4 +1,4 @@ -name: OCM Test Invite Link NC v27.1.11 to oCIS v5.0.9 +name: OCM Test Invite Link oCIS v5.0.9 to OC SM v10.15.0 # Controls when the action will run. on: @@ -28,14 +28,14 @@ jobs: matrix: sender: [ { - platform: nextcloud, - version: v27.1.11 + platform: ocis, + version: v5.0.9 }, ] receiver: [ { - platform: ocis, - version: v5.0.9 + platform: owncloud-sm, + version: v10.15.0-sm }, ] diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-ocis-5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-ocis-5.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-v27-to-owncloud-v10.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-nextcloud-v27.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-ocis-5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-ocis-5.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-5-to-owncloud-v10.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-nextcloud-v27.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-ocis-5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-v10-to-ocis-5.cy.js rename to cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js From 7ac84d58b3f6315e509bd9f6e0406c3282a737ef Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 20:36:51 +0330 Subject: [PATCH 173/184] add: check for input token to exist before clicking on accept button --- .../e2e/invite-link/ocis-v5-to-ocis-v5.cy.js | 4 ++++ .../ocm-test-suite/cypress/e2e/utils/ocis-5.js | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js index 697187c0..9cd1b9fb 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js @@ -30,6 +30,10 @@ describe('Invite link federated sharing via ScienceMesh functionality for oCIS', // load invite token from file. cy.readFile('invite-link-ocis-ocis.txt').then((token) => { + // Verify token exists and is not empty + expect(token).to.exist + expect(token.trim()).to.not.be.empty + cy.log('Read token from file:', token) cy.loginOcis('https://ocis2.docker', 'marie', 'radioactivity') diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/ocis-5.js b/cypress/ocm-test-suite/cypress/e2e/utils/ocis-5.js index 93245706..e6d91585 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/ocis-5.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/ocis-5.js @@ -52,18 +52,29 @@ export function createLegacyInviteLinkV5(domain, providerDomain) { export function acceptInviteLinkV5(token) { openScienceMeshAppV5() + // Log the token for debugging + cy.log('Attempting to use token:', token) + getScienceMeshAcceptInvitePartV5('label', 'token').within(() => { cy.get('input[type="text"]') - .type(token) + .clear() // Clear any existing value + .type(token, { delay: 100 }) // Type slower to ensure input + .should('have.value', token) // Verify the value is actually set }) + // Wait a bit after token verification + cy.wait(1000) + getScienceMeshAcceptInvitePartV5('label', 'institution').within(() => { cy.get('div[class="vs__actions"').should('be.visible').click() cy.get('ul[role="listbox"]').find('li').first().should('be.visible').click() }) - getScienceMeshAcceptInvitePartV5('span', 'accept').click() + // Wait for button to be enabled after valid input + getScienceMeshAcceptInvitePartV5('span', 'accept') + .should('not.be.disabled') + .click() } export function verifyFederatedContactV5(name, domain) { From 6d7073b28afaba0829fa573ca33e5d9e42fa1d8c Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 20:46:19 +0330 Subject: [PATCH 174/184] modify: extract domain without protocol or trailing slash --- .../nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js | 12 ++++++++---- .../owncloud-sm-v10-to-owncloud-sm-v10.cy.js | 10 +++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js index ca44bae8..91225f93 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js @@ -32,6 +32,10 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl const originalFileName = 'welcome.txt'; const sharedFileName = 'invite-link-nc-nc.txt'; + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + /** * Test case: Sending an invitation link from sender to recipient. */ @@ -58,7 +62,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl const expectedContactDisplayName = senderUsername; // Extract domain without protocol or trailing slash // Note: The 'reva' prefix is added to the expected contact domain as per application behavior - const expectedContactDomain = `reva${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`; + const expectedContactDomain = `reva${senderDomain}`; // Step 1: Load the invite link from the saved file cy.readFile(inviteLinkFileName).then((inviteLink) => { @@ -73,7 +77,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl // Step 5: Verify that the sender is now a contact in the recipient's contacts list verifyFederatedContactV27( - recipientUrl.replace(/^https?:\/\/|\/$/g, ''), + recipientDomain, expectedContactDisplayName, expectedContactDomain ); @@ -99,9 +103,9 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl // Step 5: Create a federated share for the recipient via ScienceMesh // Note: The 'reva' prefix is added to the recipient domain as per application behavior createScienceMeshShareV27( - senderUrl.replace(/^https?:\/\/|\/$/g, ''), + senderDomain, recipientUsername, - `reva${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + `reva${recipientDomain}`, sharedFileName ); diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js index 9cdac5d2..d96cc4cc 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js @@ -31,6 +31,10 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo const originalFileName = 'welcome.txt'; const sharedFileName = 'invite-link-oc-oc.txt'; + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + /** * Test case: Sending an invitation link from sender to recipient. */ @@ -57,7 +61,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo const expectedContactDisplayName = senderUsername; // Extract domain without protocol or trailing slash // Note: The 'reva' prefix is added to the expected contact domain as per application behavior - const expectedContactDomain = `reva${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`; + const expectedContactDomain = `reva${senderDomain}`; // Step 1: Read the invite link from the file cy.readFile(inviteLinkFileName).then((inviteLink) => { @@ -72,7 +76,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo // Step 5: Verify that the sender is now a contact in the recipient's contacts list verifyFederatedContact( - recipientUrl.replace(/^https?:\/\/|\/$/g, ''), + recipientDomain, expectedContactDisplayName, expectedContactDomain ); @@ -100,7 +104,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo createScienceMeshShare( sharedFileName, recipientUsername, - `reva${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + `reva${recipientDomain}`, ); // TODO @MahdiBaghbani: Verify that the share was created successfully From b9d1ac709446025fdad4d32144859539e0f06796 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 28 Jan 2025 20:47:13 +0330 Subject: [PATCH 175/184] refactor: use environment varaibles and add comments --- .../e2e/invite-link/ocis-v5-to-ocis-v5.cy.js | 183 +++++++++++++----- 1 file changed, 131 insertions(+), 52 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js index 9cd1b9fb..90155596 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js @@ -1,3 +1,12 @@ +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality in oCIS v5. + * This suite covers sending and accepting invitation links, sharing files via ScienceMesh, + * and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { openFilesAppV5, openScienceMeshAppV5, @@ -11,64 +20,134 @@ import { } from '../utils/ocis-5' describe('Invite link federated sharing via ScienceMesh functionality for oCIS', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCIS1_URL') || 'https://ocis1.docker'; + const recipientUrl = Cypress.env('OCIS2_URL') || 'https://ocis2.docker'; + const senderUsername = Cypress.env('OCIS1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('OCIS1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OCIS2_USERNAME') || 'marie'; + const recipientPassword = Cypress.env('OCIS2_PASSWORD') || 'radioactivity'; + + // Display names might be different from usernames + const senderDisplayName = Cypress.env('OCIS1_DISPLAY_NAME') || 'Albert Einstein'; + const recipientDisplayName = Cypress.env('OCIS2_DISPLAY_NAME') || 'Marie Skłodowska Curie'; + + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + + // File-related constants + const inviteLinkFileName = 'invite-link-ocis-ocis.txt'; + const sharedFileName = inviteLinkFileName; + const sharedFileContent = 'Hello World!'; + + /** + * Test case: Sending an invitation token from sender to recipient. + * Steps: + * 1. Log in to the sender's oCIS instance + * 2. Navigate to the ScienceMesh app + * 3. Generate the invite token and save it to a file + */ it('Send invitation from oCIS v5 to oCIS v5', () => { - - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') - - openScienceMeshAppV5() - - createInviteTokenV5().then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-ocis-ocis.txt', result) - } - ) - cy.wait(5000) - }) - + // Step 1: Log in to the sender's oCIS instance + cy.loginOcis(senderUrl, senderUsername, senderPassword); + + // Step 2: Navigate to the ScienceMesh app + openScienceMeshAppV5(); + + // Step 3: Generate the invite token and save it to a file + createInviteTokenV5().then((token) => { + // Ensure the token is not empty + expect(token).to.be.a('string').and.not.be.empty; + // Save the token to a file for later use + cy.writeFile(inviteLinkFileName, token); + }); + + // Wait for the operation to complete + cy.wait(5000); + }); + + /** + * Test case: Accepting the invitation token on the recipient's side. + * Steps: + * 1. Load the invite token from the saved file + * 2. Log in to the recipient's oCIS instance + * 3. Accept the invitation + * 4. Verify the federated contact is established + */ it('Accept invitation from oCIS v5 to oCIS v5', () => { - - // load invite token from file. - cy.readFile('invite-link-ocis-ocis.txt').then((token) => { + // Step 1: Load the invite token from the saved file + cy.readFile(inviteLinkFileName).then((token) => { // Verify token exists and is not empty - expect(token).to.exist - expect(token.trim()).to.not.be.empty - cy.log('Read token from file:', token) - - cy.loginOcis('https://ocis2.docker', 'marie', 'radioactivity') - - acceptInviteLinkV5(token) - - verifyFederatedContactV5('Albert Einstein', 'ocis1.docker') - }) - cy.wait(5000) - }) - + expect(token).to.exist; + expect(token.trim()).to.not.be.empty; + cy.log('Read token from file:', token); + + // Step 2: Log in to the recipient's oCIS instance + cy.loginOcis(recipientUrl, recipientUsername, recipientPassword); + + // Step 3: Accept the invitation + acceptInviteLinkV5(token); + + // Step 4: Verify the federated contact is established + verifyFederatedContactV5(senderDisplayName, senderDomain); + }); + + // Wait for the operation to complete + cy.wait(5000); + }); + + /** + * Test case: Sharing a file via ScienceMesh from sender to recipient. + * Steps: + * 1. Log in to the sender's oCIS instance + * 2. Create a text file with content + * 3. Navigate to the Files app + * 4. Share the file with the recipient + */ it('Send ScienceMesh share from oCIS v5 to oCIS v5', () => { - // share from oCIS 1. - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') - - createTextFileV5('invite-link-ocis-ocis.txt', 'Hello World!') - - openFilesAppV5() - - createShareV5('invite-link-ocis-ocis.txt', 'marie') - cy.wait(5000) - }) - + // Step 1: Log in to the sender's oCIS instance + cy.loginOcis(senderUrl, senderUsername, senderPassword); + + // Step 2: Create a text file with content + createTextFileV5(sharedFileName, sharedFileContent); + + // Step 3: Navigate to the Files app + openFilesAppV5(); + + // Step 4: Share the file with the recipient + createShareV5(sharedFileName, recipientUsername); + + // Wait for the operation to complete + cy.wait(5000); + }); + + /** + * Test case: Receiving and verifying the ScienceMesh share on the recipient's side. + * Steps: + * 1. Log in to the recipient's oCIS instance + * 2. Accept the shared file + * 3. Reload the page to refresh the view + * 4. Verify the share details are correct + */ it('Receive ScienceMesh share from oCIS v5 to oCIS v5', () => { - // accept share from oCIS 2. - cy.loginOcis('https://ocis2.docker', 'marie', 'radioactivity') + // Step 1: Log in to the recipient's oCIS instance + cy.loginOcis(recipientUrl, recipientUsername, recipientPassword); - acceptShareV5('invite-link-ocis-ocis.txt') + // Step 2: Accept the shared file + acceptShareV5(sharedFileName); - cy.reload(true) + // Step 3: Reload the page to refresh the view + cy.reload(true); + // Step 4: Verify the share details are correct verifyShareV5( - 'invite-link-ocis-ocis.txt', - 'Albert Einstein', - 'Marie Skłodowska Curie' - ) - cy.wait(5000) - }) -}) + sharedFileName, + senderDisplayName, + recipientDisplayName + ); + + // Wait for the operation to complete + cy.wait(5000); + }); +}); From 7c7a45ba09f88ed73f67c832e3bc11cae559862a Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 29 Jan 2025 00:14:03 +0330 Subject: [PATCH 176/184] refactor: add in all remaining ocis and sciencemesh tests --- ...nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js | 4 +- .../nextcloud-sm-v27-to-ocis-v5.cy.js | 166 ++++++-- .../nextcloud-sm-v27-to-owncloud-sm-v10.cy.js | 63 ++- .../ocis-v5-to-nextcloud-sm-v27.cy.js | 210 +++++++--- .../ocis-v5-to-owncloud-sm-v10.cy.js | 183 +++++--- .../owncloud-sm-v10-to-nextcloud-sm-v27.cy.js | 75 ++-- .../owncloud-sm-v10-to-ocis-v5.cy.js | 164 ++++++-- .../owncloud-sm-v10-to-owncloud-sm-v10.cy.js | 8 +- .../invite-link/nextcloud-ocis.sh | 392 ------------------ .../invite-link/nextcloud-owncloud.sh | 324 --------------- .../invite-link/nextcloud-sm-nextcloud-sm.sh | 4 +- .../invite-link/nextcloud-sm-ocis.sh | 144 +++++++ .../invite-link/nextcloud-sm-owncloud-sm.sh | 143 +++++++ .../invite-link/ocis-nextcloud-sm.sh | 144 +++++++ .../invite-link/ocis-nextcloud.sh | 392 ------------------ dev/ocm-test-suite/invite-link/ocis-ocis.sh | 37 +- .../invite-link/ocis-owncloud-sm.sh | 144 +++++++ .../invite-link/ocis-owncloud.sh | 390 ----------------- .../invite-link/owncloud-nextcloud.sh | 324 --------------- .../invite-link/owncloud-ocis.sh | 391 ----------------- .../invite-link/owncloud-sm-nextcloud-sm.sh | 143 +++++++ .../invite-link/owncloud-sm-ocis.sh | 144 +++++++ 22 files changed, 1498 insertions(+), 2491 deletions(-) delete mode 100755 dev/ocm-test-suite/invite-link/nextcloud-ocis.sh delete mode 100755 dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh create mode 100644 dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh create mode 100644 dev/ocm-test-suite/invite-link/nextcloud-sm-owncloud-sm.sh create mode 100644 dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh delete mode 100755 dev/ocm-test-suite/invite-link/ocis-nextcloud.sh create mode 100644 dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh delete mode 100755 dev/ocm-test-suite/invite-link/ocis-owncloud.sh delete mode 100755 dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh delete mode 100755 dev/ocm-test-suite/invite-link/owncloud-ocis.sh create mode 100644 dev/ocm-test-suite/invite-link/owncloud-sm-nextcloud-sm.sh create mode 100644 dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js index 91225f93..42dd6356 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-nextcloud-sm-v27.cy.js @@ -87,7 +87,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl /** * Test case: Sharing a file via ScienceMesh from sender to recipient. */ - it('Send ScienceMesh share of a file from Nextcloud v27 to Nextcloud v27', () => { + it('Send ScienceMesh share of a from Nextcloud v27 to Nextcloud v27', () => { // Step 1: Log in to the sender's Nextcloud instance cy.loginNextcloud(senderUrl, senderUsername, senderPassword); @@ -115,7 +115,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl /** * Test case: Receiving and verifying the ScienceMesh share on the recipient's side. */ - it('Receive ScienceMesh share of a file from Nextcloud v27 to Nextcloud v27', () => { + it('Receive ScienceMesh share of a from Nextcloud v27 to Nextcloud v27', () => { // Step 1: Log in to the recipient's Nextcloud instance cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js index 9ad20a1e..508b61fa 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js @@ -1,63 +1,155 @@ +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality + * between Nextcloud v27 and oCIS v5. This suite covers sending and accepting invitation links, + * sharing files via ScienceMesh, and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { createInviteTokenV27, createScienceMeshShareV27, - renameFileV27 -} from '../utils/nextcloud-v27' + renameFileV27, + ensureFileExistsV27, +} from '../utils/nextcloud-v27'; import { + openFilesAppV5, acceptInviteLinkV5, verifyFederatedContactV5, - acceptShareV5, - verifyShareV5 -} from '../utils/ocis-5' +} from '../utils/ocis-5'; + +describe('Invite link federated sharing via ScienceMesh functionality between Nextcloud and oCIS', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const recipientUrl = Cypress.env('OCIS1_URL') || 'https://ocis1.docker'; + const senderUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'marie'; + const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'radioactivity'; + const recipientUsername = Cypress.env('OCIS1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('OCIS1_PASSWORD') || 'relativity'; + + // Display names might be different from usernames + const senderDisplayName = Cypress.env('NEXTCLOUD1_DISPLAY_NAME') || 'marie'; + const recipientDisplayName = Cypress.env('OCIS1_DISPLAY_NAME') || 'Albert Einstein'; + + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + + // File-related constants + const inviteLinkFileName = 'invite-link-nc-ocis.txt'; + const originalFileName = 'welcome.txt'; + const sharedFileName = inviteLinkFileName; -describe('Invite link federated sharing via ScienceMesh functionality for oCIS', () => { + /** + * Test case: Sending an invitation link from Nextcloud to oCIS. + * Steps: + * 1. Log in to the sender's Nextcloud instance + * 2. Navigate to the ScienceMesh app + * 3. Generate the invite link and save it to a file + */ it('Send invitation from Nextcloud v27 to oCIS v5', () => { + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); - cy.loginNextcloud('https://nextcloud1.docker', 'marie', 'radioactivity') - cy.visit('https://nextcloud1.docker/index.php/apps/sciencemesh/contacts') + // Step 2: Navigate to the ScienceMesh app + cy.visit(`${senderUrl}/index.php/apps/sciencemesh/contacts`); - createInviteTokenV27().then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-nc-ocis.txt', result) - } - ) - }) + // Step 3: Generate the invite token and save it to a file + createInviteTokenV27().then((inviteToken) => { + // Ensure the invite token is not empty + expect(inviteToken).to.be.a('string').and.not.be.empty; + // Save the invite token to a file for later use + cy.writeFile(inviteLinkFileName, inviteToken); + }); + }); + /** + * Test case: Accepting the invitation link on oCIS side. + * Steps: + * 1. Load the invite link from the saved file + * 2. Log in to the recipient's oCIS instance + * 3. Accept the invitation + * 4. Verify the federated contact is established + */ it('Accept invitation from Nextcloud v27 to oCIS v5', () => { + // Step 1: Load the invite token from the saved file + cy.readFile(inviteLinkFileName).then((token) => { + // Verify token exists and is not empty + expect(token).to.exist; + expect(token.trim()).to.not.be.empty; + cy.log('Read token from file:', token); - // load invite token from file. - cy.readFile('invite-link-nc-ocis.txt').then((token) => { + // Step 2: Log in to the recipient's oCIS instance + cy.loginOcis(recipientUrl, recipientUsername, recipientPassword); - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') + // Step 3: Accept the invitation + acceptInviteLinkV5(token); - acceptInviteLinkV5(token) + // Step 4: Verify the federated contact is established + verifyFederatedContactV5(senderDisplayName, senderDomain); + }); - verifyFederatedContactV5('marie', 'revanextcloud1.docker') - }) - }) + // Wait for the operation to complete + cy.wait(5000); + }); + /** + * Test case: Sharing a file via ScienceMesh from Nextcloud to oCIS. + * Steps: + * 1. Log in to the sender's Nextcloud instance + * 2. Ensure the original file exists + * 3. Rename the file for sharing + * 4. Create the share for the recipient + */ it('Send ScienceMesh share from Nextcloud v27 to oCIS v5', () => { - // share from Nextcloud 1. - cy.loginNextcloud('https://nextcloud1.docker', 'marie', 'radioactivity') + // Step 1: Log in to the sender's Nextcloud instance + cy.loginNextcloud(senderUrl, senderUsername, senderPassword); - renameFileV27('welcome.txt', 'invite-link-nc-ocis.txt') - createScienceMeshShareV27('nextcloud1.docker', 'Albert Einstein', 'https://ocis1.docker', 'invite-link-nc-ocis.txt') - }) + // Step 2: Ensure the original file exists + ensureFileExistsV27(originalFileName); + // Step 3: Rename the file for sharing + renameFileV27(originalFileName, sharedFileName); + + // Step 4: Verify the file has been renamed + ensureFileExistsV27(sharedFileName); + + // Step 5: Create the share for the recipient + createScienceMeshShareV27( + senderDomain, + recipientUsername, + recipientDomain, + sharedFileName + ); + }); + + /** + * Test case: Receiving and verifying the ScienceMesh share on oCIS side. + * Steps: + * 1. Log in to the recipient's oCIS instance + * 2. Navigate to the Files app + * 3. Verify the shared file exists + */ it('Receive ScienceMesh share from Nextcloud v27 to oCIS v5', () => { - // accept share from oCIS 1. - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') + // Step 1: Log in to the recipient's oCIS instance + cy.loginOcis(recipientUrl, recipientUsername, recipientPassword); - acceptShareV5('invite-link-nc-ocis.txt') + // Step 2: Accept the shared file + acceptShareV5(sharedFileName); - cy.reload(true) + // Step 3: Reload the page to refresh the view + cy.reload(true); + // Step 4: Verify the share details are correct verifyShareV5( - 'invite-link-nc-ocis.txt', - 'marie', - 'Albert Einstein' - ) - }) -}) + sharedFileName, + senderDisplayName, + recipientDisplayName + ); + + // Wait for the operation to complete + cy.wait(5000); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js index 72bfe3e2..5e125786 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js @@ -1,8 +1,8 @@ /** * @fileoverview - * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality in Nextcloud v27 and ownCloud v10. - * This suite covers sending and accepting invitation links, sharing files via ScienceMesh, - * and verifying that the shares are received correctly. + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality + * between Nextcloud v27 and ownCloud v10. This suite covers sending and accepting invitation links, + * sharing files via ScienceMesh, and verifying that the shares are received correctly. * * @author Mohammad Mahdi Baghbani Pourvahid */ @@ -22,8 +22,7 @@ import { selectAppFromLeftSide, } from '../utils/owncloud'; -describe('Invite link federated sharing via ScienceMesh functionality for Nextcloud to ownCloud', () => { - +describe('Invite link federated sharing via ScienceMesh functionality between Nextcloud and ownCloud', () => { // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; const recipientUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; @@ -31,12 +30,22 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl const senderPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; const recipientUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; const recipientPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + + // File-related constants const inviteLinkFileName = 'invite-link-nc-oc.txt'; const originalFileName = 'welcome.txt'; - const sharedFileName = 'invite-link-nc-oc.txt'; + const sharedFileName = inviteLinkFileName; /** - * Test case: Sending an invitation link from sender to recipient. + * Test case: Sending an invitation link from Nextcloud to ownCloud. + * Steps: + * 1. Log in to the sender's Nextcloud instance + * 2. Navigate to the ScienceMesh app + * 3. Generate the invite link and save it to a file */ it('Send invitation from Nextcloud v27 to ownCloud v10', () => { // Step 1: Log in to the sender's Nextcloud instance @@ -55,13 +64,18 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl }); /** - * Test case: Accepting the invitation link on the recipient's side. + * Test case: Accepting the invitation link on ownCloud side. + * Steps: + * 1. Load the invite link from the saved file + * 2. Log in to the recipient's ownCloud instance + * 3. Accept the invitation + * 4. Verify the federated contact is established */ - it('Accept invitation from from Nextcloud v27 v10 to ownCloud v10', () => { + it('Accept invitation from Nextcloud v27 to ownCloud v10', () => { const expectedContactDisplayName = senderUsername; // Extract domain without protocol or trailing slash // Note: The 'reva' prefix is added to the expected contact domain as per application behavior - const expectedContactDomain = `reva${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`; + const expectedContactDomain = `reva${senderDomain}`; // Step 1: Read the invite link from the file cy.readFile(inviteLinkFileName).then((inviteLink) => { @@ -76,7 +90,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl // Step 5: Verify that the sender is now a contact in the recipient's contacts list verifyFederatedContact( - recipientUrl.replace(/^https?:\/\/|\/$/g, ''), + recipientDomain, expectedContactDisplayName, expectedContactDomain ); @@ -84,9 +98,14 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl }); /** - * Test case: Sharing a file via ScienceMesh from sender to recipient. + * Test case: Sharing a file via ScienceMesh from Nextcloud to ownCloud. + * Steps: + * 1. Log in to the sender's Nextcloud instance + * 2. Ensure the original file exists + * 3. Rename the file for sharing + * 4. Create the share for the recipient */ - it('Send ScienceMesh share of a file from Nextcloud v27 to ownCloud v10', () => { + it('Send ScienceMesh share from Nextcloud v27 to ownCloud v10', () => { // Step 1: Log in to the sender's Nextcloud instance cy.loginNextcloud(senderUrl, senderUsername, senderPassword); @@ -102,20 +121,20 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl // Step 5: Create a federated share for the recipient via ScienceMesh // Note: The 'reva' prefix is added to the recipient domain as per application behavior createScienceMeshShareV27( - senderUrl.replace(/^https?:\/\/|\/$/g, ''), + senderDomain, recipientUsername, - `reva${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + `reva${recipientDomain}`, sharedFileName ); - - // TODO @MahdiBaghbani: Verify that the share was created successfully }); /** - * Test Case: Receiving and accepting a ScienceMesh file share on ownCloud. - * This test verifies that the shared file appears in the "Sharing In" section. + * Test case: Receiving and verifying the ScienceMesh share on ownCloud side. + * Steps: + * 1. Log in to the recipient's ownCloud instance + * 2. Verify the shared file exists and has correct sharing information */ - it('Receive ScienceMesh share of a file from Nextcloud v27 to ownCloud v10', () => { + it('Receive ScienceMesh share from Nextcloud v27 to ownCloud v10', () => { // Step 1: Log in to the recipient's ownCloud instance cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); @@ -127,7 +146,5 @@ describe('Invite link federated sharing via ScienceMesh functionality for Nextcl // Step 4: Verify that the shared file is visible ensureFileExists(sharedFileName); - - // TODO @MahdiBaghbani: Download or open the file to verify content (if required) }); -}) +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js index 14b5d28b..44f17f5b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js @@ -1,78 +1,156 @@ +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality + * between oCIS v5 and Nextcloud v27. This suite covers sending and accepting invitation links, + * sharing files via ScienceMesh, and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { openFilesAppV5, openScienceMeshAppV5, createLegacyInviteLinkV5, createTextFileV5, createShareV5, -} from '../utils/ocis-5' +} from '../utils/ocis-5'; import { + acceptShareV27, + verifyFederatedContactV27, + acceptScienceMeshInvitation, + ensureFileExistsV27, navigationSwitchLeftSideV27, selectAppFromLeftSideV27, -} from '../utils/nextcloud-v27' - -describe('Invite link federated sharing via ScienceMesh functionality for oCIS', () => { +} from '../utils/nextcloud-v27'; + +describe('Invite link federated sharing via ScienceMesh functionality between oCIS and Nextcloud', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCIS1_URL') || 'https://ocis1.docker'; + const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; + const senderUsername = Cypress.env('OCIS1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('OCIS1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'michiel'; + const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'dejong'; + + // Display names might be different from usernames + const senderDisplayName = Cypress.env('OCIS1_DISPLAY_NAME') || 'Albert Einstein'; + + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + + // File-related constants + const inviteLinkFileName = 'invite-link-ocis-nc.txt'; + const sharedFileName = inviteLinkFileName; + const sharedFileContent = 'Hello World!'; + + /** + * Test case: Sending an invitation token from oCIS to Nextcloud. + * Steps: + * 1. Log in to the sender's oCIS instance + * 2. Navigate to the ScienceMesh app + * 3. Generate the invite token and save it to a file + */ it('Send invitation from oCIS v5 to Nextcloud v27', () => { - - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') - - openScienceMeshAppV5() - - createLegacyInviteLinkV5('nextcloud1.docker', 'ocis1.docker').then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-ocis-nc.txt', result) - } - ) - }) - + // Step 1: Log in to the sender's oCIS instance + cy.loginOcis(senderUrl, senderUsername, senderPassword); + + // Step 2: Navigate to the ScienceMesh app + openScienceMeshAppV5(); + + // Step 3: Generate the invite link and save it to a file + createLegacyInviteLinkV5(recipientDomain, senderDomain).then((inviteLink) => { + // Ensure the invite link is not empty + expect(inviteLink).to.be.a('string').and.not.be.empty; + // Save the invite link to a file for later use + cy.writeFile(inviteLinkFileName, inviteLink); + }); + + // Wait for the operation to complete + cy.wait(5000); + }); + + /** + * Test case: Accepting the invitation token on Nextcloud side. + * Steps: + * 1. Load the invite token from the saved file + * 2. Log in to the recipient's Nextcloud instance + * 3. Accept the invitation + * 4. Verify the federated contact is established + */ it('Accept invitation from oCIS v5 to Nextcloud v27', () => { - - // load invite link from file. - cy.readFile('invite-link-ocis-nc.txt').then((url) => { - - // accept invitation from Nextcloud 1. - cy.loginNextcloudCore(url, 'marie', 'radioactivity') - - cy.get('input[id="accept-button"]', { timeout: 10000 }) - .click() - - // validate 'Albert Einstein' is shown as a contact. - cy.visit('https://nextcloud1.docker/index.php/apps/sciencemesh/contacts') - - cy.get('table[id="contact-table"]') - .find('p[class="displayname"]') - .should("have.text", "Albert Einstein"); - }) - }) - - // it('Send ScienceMesh share from oCIS v5 to Nextcloud v27', () => { - // // share from oCIS 1. - // cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') - - // createTextFileV5('invite-link-ocis-nc.txt', 'Hello World!') - - // openFilesAppV5() - - // createShareV5('invite-link-ocis-nc.txt', 'marie') - - // cy.wait(5000) - // }) - - // it('Receive ScienceMesh share from oCIS v5 to Nextcloud v27', () => { - // // accept share from Nextcloud 1. - // cy.loginNextcloud('https://nextcloud1.docker', 'marie', 'radioactivity') - - // cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - // .should('be.visible') - // .find('*[class^="oc-dialog-buttonrow"]') - // .find('button[class="primary"]') - // .click() - - // navigationSwitchLeftSideV27('Open navigation') - // selectAppFromLeftSideV27('shareoverview') - // navigationSwitchLeftSideV27('Close navigation') - - // cy.get('[data-file="invite-link-ocis-nc.txt"]', { timeout: 10000 }).should('be.visible') - // }) -}) + const expectedContactDisplayName = senderDisplayName; + // Extract domain without protocol or trailing slash + // Note: The 'reva' prefix is added to the expected contact domain as per application behavior + const expectedContactDomain = senderDomain; + + // Step 1: Load the invite link from the file + cy.readFile(inviteLinkFileName).then((inviteLink) => { + // Step 2: Ensure the invite link is valid + expect(inviteLink).to.be.a('string').and.not.be.empty; + + // Step 3: Login to the recipient's Nextcloud instance using the invite link + cy.loginNextcloudCore(inviteLink, recipientUsername, recipientPassword); + + // Step 4: Accept the invitation + acceptScienceMeshInvitation(); + + // Step 5: Verify that the sender is now a contact in the recipient's contacts list + verifyFederatedContactV27( + recipientDomain, + expectedContactDisplayName, + expectedContactDomain + ); + }); + }); + + /** + * Test case: Sharing a file via ScienceMesh from oCIS to Nextcloud. + * Steps: + * 1. Log in to the sender's oCIS instance + * 2. Create a text file with content + * 3. Navigate to the Files app + * 4. Share the file with the recipient + */ + it('Send ScienceMesh share from oCIS v5 to Nextcloud v27', () => { + // Step 1: Log in to the sender's oCIS instance + cy.loginOcis(senderUrl, senderUsername, senderPassword); + + // Step 2: Create a text file with content + createTextFileV5(sharedFileName, sharedFileContent); + + // Step 3: Navigate to the Files app + openFilesAppV5(); + + // Step 4: Share the file with the recipient + createShareV5(sharedFileName, recipientUsername); + + // Wait for the operation to complete + cy.wait(5000); + }); + + /** + * Test case: Receiving and verifying the ScienceMesh share on Nextcloud side. + * Steps: + * 1. Log in to the recipient's Nextcloud instance + * 2. Accept the shared file + * 3. Navigate to the correct section + * 4. Verify the shared file exists + */ + it('Receive ScienceMesh share from oCIS v5 to Nextcloud v27', () => { + // Step 1: Log in to the recipient's Nextcloud instance + cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); + + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShareV27(); + + // Step 3: Navigate to the correct section + navigationSwitchLeftSideV27('Open navigation'); + selectAppFromLeftSideV27('files'); + navigationSwitchLeftSideV27('Close navigation'); + + // Step 4: Verify the shared file is visible + ensureFileExistsV27(sharedFileName); + }); +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js index 26c84a06..4fb795b4 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js @@ -1,3 +1,12 @@ +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality + * between oCIS v5 and ownCloud v10. This suite covers sending and accepting invitation links, + * sharing files via ScienceMesh, and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { openFilesAppV5, openScienceMeshAppV5, @@ -7,68 +16,136 @@ import { } from '../utils/ocis-5' import { - selectAppFromLeftSide -} from '../utils/owncloud' - -describe('Invite link federated sharing via ScienceMesh functionality for oCIS', () => { + acceptShare, + verifyFederatedContact, + acceptScienceMeshInvitation, + ensureFileExists, + selectAppFromLeftSide, +} from '../utils/owncloud'; + +describe('Invite link federated sharing via ScienceMesh functionality between oCIS and ownCloud', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OCIS1_URL') || 'https://ocis1.docker'; + const recipientUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const senderUsername = Cypress.env('OCIS1_USERNAME') || 'einstein'; + const senderPassword = Cypress.env('OCIS1_PASSWORD') || 'relativity'; + const recipientUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const recipientPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + + // Display names might be different from usernames + const senderDisplayName = Cypress.env('OCIS1_DISPLAY_NAME') || 'Albert Einstein'; + + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + + // File-related constants + const inviteLinkFileName = 'invite-link-ocis-oc.txt'; + const sharedFileName = inviteLinkFileName; + const sharedFileContent = 'Hello World!'; + + /** + * Test case: Sending an invitation token from oCIS to ownCloud. + * Steps: + * 1. Log in to the sender's oCIS instance + * 2. Navigate to the ScienceMesh app + * 3. Generate the invite token and save it to a file + */ it('Send invitation from oCIS v5 to ownCloud v10', () => { - - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') - - openScienceMeshAppV5() - - createLegacyInviteLinkV5('owncloud1.docker', 'ocis1.docker').then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-ocis-oc.txt', result) - } - ) - }) - + // Step 1: Log in to the sender's oCIS instance + cy.loginOcis(senderUrl, senderUsername, senderPassword); + + // Step 2: Navigate to the ScienceMesh app + openScienceMeshAppV5(); + + // Step 3: Generate the invite link and save it to a file + createLegacyInviteLinkV5(recipientDomain, senderDomain).then((inviteLink) => { + // Ensure the invite link is not empty + expect(inviteLink).to.be.a('string').and.not.be.empty; + // Save the invite link to a file for later use + cy.writeFile(inviteLinkFileName, inviteLink); + }); + + // Wait for the operation to complete + cy.wait(5000); + }); + + /** + * Test case: Accepting the invitation token on ownCloud side. + * Steps: + * 1. Load the invite token from the saved file + * 2. Log in to the recipient's ownCloud instance + * 3. Accept the invitation + * 4. Verify the federated contact is established + */ it('Accept invitation from oCIS v5 to ownCloud v10', () => { - - // load invite link from file. - cy.readFile('invite-link-ocis-oc.txt').then((url) => { - - // accept invitation from ownCloud 1. - cy.loginOwncloudCore(url, 'marie', 'radioactivity') - - cy.get('input[id="accept-button"]', { timeout: 10000 }) - .click() - - // validate 'Albert Einstein' is shown as a contact. - cy.visit('https://owncloud1.docker/index.php/apps/sciencemesh/contacts') - - cy.get('table[id="contact-table"]') - .find('p[class="displayname"]') - .should("have.text", "Albert Einstein"); - }) - }) - + const expectedContactDisplayName = senderDisplayName; + // Extract domain without protocol or trailing slash + // Note: The 'reva' prefix is added to the expected contact domain as per application behavior + const expectedContactDomain = senderDomain; + + // Step 1: Read the invite link from the file + cy.readFile(inviteLinkFileName).then((inviteLink) => { + // Step 2: Ensure the invite link is valid + expect(inviteLink).to.be.a('string').and.not.be.empty; + + // Step 3: Login to the recipient's ownCloud instance using the invite link + cy.loginOwncloudCore(inviteLink, recipientUsername, recipientPassword); + + // Step 4: Accept the invitation + acceptScienceMeshInvitation(); + + // Step 5: Verify that the sender is now a contact in the recipient's contacts list + verifyFederatedContact( + recipientDomain, + expectedContactDisplayName, + expectedContactDomain + ); + }); + }); + + /** + * Test case: Sharing a file via ScienceMesh from oCIS to ownCloud. + * Steps: + * 1. Log in to the sender's oCIS instance + * 2. Create a text file with content + * 3. Navigate to the Files app + * 4. Share the file with the recipient + */ it('Send ScienceMesh share from oCIS v5 to ownCloud v10', () => { - // share from oCIS 1. - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') + // Step 1: Log in to the sender's oCIS instance + cy.loginOcis(senderUrl, senderUsername, senderPassword); + + // Step 2: Create a text file with content + createTextFileV5(sharedFileName, sharedFileContent); - createTextFileV5('invite-link-ocis-oc.txt', 'Hello World!') + // Step 3: Navigate to the Files app + openFilesAppV5(); - openFilesAppV5() + // Step 4: Share the file with the recipient + createShareV5(sharedFileName, recipientUsername); - createShareV5('invite-link-ocis-oc.txt', 'marie') - }) + // Wait for the operation to complete + cy.wait(5000); + }); + /** + * Test case: Receiving and verifying the ScienceMesh share on ownCloud side. + * Steps: + * 1. Log in to the recipient's ownCloud instance + * 2. Verify the shared file exists and has correct sharing information + */ it('Receive ScienceMesh share from oCIS v5 to ownCloud v10', () => { - // accept share from ownCloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') + // Step 1: Log in to the recipient's ownCloud instance + cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); - cy.get('div[class="oc-dialog"]', { timeout: 10000 }) - .should('be.visible') - .find('*[class^="oc-dialog-buttonrow"]') - .find('button[class="primary"]') - .click() + // Step 2: Wait for the share dialog to appear and accept the incoming federated share + acceptShare(); - selectAppFromLeftSide('sharingin') + // Step 3: Navigate to the correct section + selectAppFromLeftSide('files'); - cy.get('[data-file="invite-link-ocis-oc.txt"]', { timeout: 10000 }) - .should('be.visible') - }) + // Step 4: Verify that the shared file is visible + ensureFileExists(sharedFileName); + }); }) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js index 13248268..0648d706 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js @@ -1,12 +1,20 @@ /** * @fileoverview - * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality in ownCloud v10 and Nextcloud v27. - * This suite covers sending and accepting invitation links, sharing files via ScienceMesh, - * and verifying that the shares are received correctly. + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality + * between ownCloud v10 and Nextcloud v27. This suite covers sending and accepting invitation links, + * sharing files via ScienceMesh, and verifying that the shares are received correctly. * * @author Mohammad Mahdi Baghbani Pourvahid */ +import { + createInviteLink, + acceptScienceMeshInvitation, + createScienceMeshShare, + renameFile, + ensureFileExists, +} from '../utils/owncloud'; + import { acceptShareV27, verifyFederatedContactV27, @@ -16,15 +24,7 @@ import { selectAppFromLeftSideV27, } from '../utils/nextcloud-v27'; -import { - createInviteLink, - createScienceMeshShare, - renameFile, - ensureFileExists, -} from '../utils/owncloud'; - -describe('Invite link federated sharing via ScienceMesh functionality for ownCloud to Nextcloud', () => { - +describe('Invite link federated sharing via ScienceMesh functionality between ownCloud and Nextcloud', () => { // Shared variables to avoid repetition and improve maintainability const senderUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; const recipientUrl = Cypress.env('NEXTCLOUD1_URL') || 'https://nextcloud1.docker'; @@ -32,14 +32,24 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo const senderPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; const recipientUsername = Cypress.env('NEXTCLOUD1_USERNAME') || 'einstein'; const recipientPassword = Cypress.env('NEXTCLOUD1_PASSWORD') || 'relativity'; + + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); + + // File-related constants const inviteLinkFileName = 'invite-link-oc-nc.txt'; const originalFileName = 'welcome.txt'; - const sharedFileName = 'invite-link-oc-nc.txt'; + const sharedFileName = inviteLinkFileName; /** - * Test case: Sending an invitation link from sender to recipient. + * Test case: Sending an invitation link from ownCloud to Nextcloud. + * Steps: + * 1. Log in to the sender's ownCloud instance + * 2. Navigate to the ScienceMesh app + * 3. Generate the invite link and save it to a file */ - it('Send invitation from from ownCloud v10 to Nextcloud v27', () => { + it('Send invitation from ownCloud v10 to Nextcloud v27', () => { // Step 1: Log in to the sender's ownCloud instance cy.loginOwncloud(senderUrl, senderUsername, senderPassword); @@ -56,13 +66,18 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo }); /** - * Test case: Accepting the invitation link on the recipient's side. + * Test case: Accepting the invitation link on Nextcloud side. + * Steps: + * 1. Load the invite link from the saved file + * 2. Log in to the recipient's Nextcloud instance + * 3. Accept the invitation + * 4. Verify the federated contact is established */ it('Accept invitation from ownCloud v10 to Nextcloud v27', () => { const expectedContactDisplayName = senderUsername; // Extract domain without protocol or trailing slash // Note: The 'reva' prefix is added to the expected contact domain as per application behavior - const expectedContactDomain = `reva${senderUrl.replace(/^https?:\/\/|\/$/g, '')}`; + const expectedContactDomain = `reva${senderDomain}`; // Step 1: Load the invite link from the saved file cy.readFile(inviteLinkFileName).then((inviteLink) => { @@ -77,7 +92,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo // Step 5: Verify that the sender is now a contact in the recipient's contacts list verifyFederatedContactV27( - recipientUrl.replace(/^https?:\/\/|\/$/g, ''), + recipientDomain, expectedContactDisplayName, expectedContactDomain ); @@ -85,9 +100,14 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo }); /** - * Test case: Sharing a file via ScienceMesh from sender to recipient. + * Test case: Sharing a file via ScienceMesh from ownCloud to Nextcloud. + * Steps: + * 1. Log in to the sender's ownCloud instance + * 2. Ensure the original file exists + * 3. Rename the file for sharing + * 4. Create the share for the recipient */ - it('Send ScienceMesh share of a file from ownCloud v10 to Nextcloud v27', () => { + it('Send ScienceMesh share from ownCloud v10 to Nextcloud v27', () => { // Step 1: Log in to the sender's ownCloud instance cy.loginOwncloud(senderUrl, senderUsername, senderPassword); @@ -105,16 +125,19 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo createScienceMeshShare( sharedFileName, recipientUsername, - `reva${recipientUrl.replace(/^https?:\/\/|\/$/g, '')}`, + `reva${recipientDomain}`, ); - - // TODO @MahdiBaghbani: Verify that the share was created successfully }); /** - * Test case: Receiving and verifying the ScienceMesh share on the recipient's side. + * Test case: Receiving and verifying the ScienceMesh share on Nextcloud side. + * Steps: + * 1. Log in to the recipient's Nextcloud instance + * 2. Accept the shared file + * 3. Navigate to the correct section + * 4. Verify the shared file exists */ - it('Receive ScienceMesh share of a file from ownCloud v10 to Nextcloud v27', () => { + it('Receive ScienceMesh share from ownCloud v10 to Nextcloud v27', () => { // Step 1: Log in to the recipient's Nextcloud instance cy.loginNextcloud(recipientUrl, recipientUsername, recipientPassword); @@ -131,4 +154,4 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo // TODO @MahdiBaghbani: Download or open the file to verify content (if required) }); -}) +}); diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js index 9f9f15e6..0d521b34 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js @@ -1,62 +1,154 @@ +/** + * @fileoverview + * Cypress test suite for testing invite link federated sharing via ScienceMesh functionality + * between ownCloud v10 and oCIS v5. This suite covers sending and accepting invitation links, + * sharing files via ScienceMesh, and verifying that the shares are received correctly. + * + * @author Mohammad Mahdi Baghbani Pourvahid + */ + import { - createInviteToken, createScienceMeshShare, - renameFile -} from '../utils/owncloud' + renameFile, + ensureFileExists, +} from '../utils/owncloud'; + import { acceptInviteLinkV5, verifyFederatedContactV5, acceptShareV5, verifyShareV5 -} from '../utils/ocis-5' +} from '../utils/ocis-5'; + +describe('Invite link federated sharing via ScienceMesh functionality between ownCloud and oCIS', () => { + // Shared variables to avoid repetition and improve maintainability + const senderUrl = Cypress.env('OWNCLOUD1_URL') || 'https://owncloud1.docker'; + const recipientUrl = Cypress.env('OCIS1_URL') || 'https://ocis1.docker'; + const senderUsername = Cypress.env('OWNCLOUD1_USERNAME') || 'marie'; + const senderPassword = Cypress.env('OWNCLOUD1_PASSWORD') || 'radioactivity'; + const recipientUsername = Cypress.env('OCIS1_USERNAME') || 'einstein'; + const recipientPassword = Cypress.env('OCIS1_PASSWORD') || 'relativity'; + + // Display names might be different from usernames + const senderDisplayName = Cypress.env('OWNCLOUD1_DISPLAY_NAME') || 'marie'; + const recipientDisplayName = Cypress.env('OCIS1_DISPLAY_NAME') || 'Albert Einstein'; + + // Extract domain without protocol or trailing slash + const senderDomain = senderUrl.replace(/^https?:\/\/|\/$/g, ''); + const recipientDomain = recipientUrl.replace(/^https?:\/\/|\/$/g, ''); -describe('Invite link federated sharing via ScienceMesh functionality for ownCloud', () => { + // File-related constants + const inviteLinkFileName = 'invite-link-oc-ocis.txt'; + const originalFileName = 'welcome.txt'; + const sharedFileName = inviteLinkFileName; + + /** + * Test case: Sending an invitation link from ownCloud to oCIS. + * Steps: + * 1. Log in to the sender's ownCloud instance + * 2. Navigate to the ScienceMesh app + * 3. Generate the invite link and save it to a file + */ it('Send invitation from ownCloud v10 to oCIS v5', () => { + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') - cy.visit('https://owncloud1.docker/index.php/apps/sciencemesh/') + // Step 2: Navigate to the ScienceMesh app + cy.visit(`${senderUrl}/index.php/apps/sciencemesh/`); - createInviteToken().then( - (result) => { - // save invite link to file. - cy.writeFile('invite-link-oc-ocis.txt', result) - } - ) - }) + // Step 3: Generate an invite token and save it to a file + createInviteToken().then((inviteToken) => { + // Step 4: Ensure the invite token is not empty + expect(inviteToken).to.be.a('string').and.not.be.empty; + // Step 5: Save the invite token to a file for later use + cy.writeFile(inviteLinkFileName, inviteToken); + }); + }); + /** + * Test case: Accepting the invitation link on oCIS side. + * Steps: + * 1. Load the invite link from the saved file + * 2. Log in to the recipient's oCIS instance + * 3. Accept the invitation + * 4. Verify the federated contact is established + */ it('Accept invitation from ownCloud v10 to oCIS v5', () => { + // Step 1: Load the invite token from the saved file + cy.readFile(inviteLinkFileName).then((token) => { + // Verify token exists and is not empty + expect(token).to.exist; + expect(token.trim()).to.not.be.empty; + cy.log('Read token from file:', token); - // load invite token from file. - cy.readFile('invite-link-oc-ocis.txt').then((token) => { + // Step 2: Log in to the recipient's oCIS instance + cy.loginOcis(recipientUrl, recipientUsername, recipientPassword); - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') + // Step 3: Accept the invitation + acceptInviteLinkV5(token); - acceptInviteLinkV5(token) + // Step 4: Verify the federated contact is established + verifyFederatedContactV5(senderDisplayName, senderDomain); + }); - verifyFederatedContactV5('marie', 'revaowncloud1.docker') - }) - }) + // Wait for the operation to complete + cy.wait(5000); + }); + /** + * Test case: Sharing a file via ScienceMesh from ownCloud to oCIS. + * Steps: + * 1. Log in to the sender's ownCloud instance + * 2. Ensure the original file exists + * 3. Rename the file for sharing + * 4. Create the share for the recipient + */ it('Send ScienceMesh share from ownCloud v10 to oCIS v5', () => { - // share from ownCloud 1. - cy.loginOwncloud('https://owncloud1.docker', 'marie', 'radioactivity') + // Step 1: Log in to the sender's ownCloud instance + cy.loginOwncloud(senderUrl, senderUsername, senderPassword); + + // Step 2: Ensure the original file exists + ensureFileExists(originalFileName); + + // Step 3: Rename the file + renameFile(originalFileName, sharedFileName); - renameFile('welcome.txt', 'invite-link-oc-ocis.txt') - createScienceMeshShare('invite-link-oc-ocis.txt', 'einstein', 'ocis1.docker') - }) + // Step 4: Verify the file has been renamed + ensureFileExists(sharedFileName); + // Step 5: Create a federated share for the recipient via ScienceMesh + createScienceMeshShare( + sharedFileName, + recipientUsername, + recipientDomain, + ); + }); + + /** + * Test case: Receiving and verifying the ScienceMesh share on oCIS side. + * Steps: + * 1. Log in to the recipient's oCIS instance + * 2. Navigate to the Files app + * 3. Verify the shared file exists + */ it('Receive ScienceMesh share from ownCloud v10 to oCIS v5', () => { - // accept share from oCIS 1. - cy.loginOcis('https://ocis1.docker', 'einstein', 'relativity') + // Step 1: Log in to the recipient's oCIS instance + cy.loginOcis(recipientUrl, recipientUsername, recipientPassword); - acceptShareV5('invite-link-oc-ocis.txt') + // Step 2: Accept the shared file + acceptShareV5(sharedFileName); - cy.reload(true) + // Step 3: Reload the page to refresh the view + cy.reload(true); + // Step 4: Verify the share details are correct verifyShareV5( - 'invite-link-oc-ocis.txt', - 'marie', - 'Albert Einstein' - ) - }) -}) \ No newline at end of file + sharedFileName, + senderDisplayName, + recipientDisplayName + ); + + // Wait for the operation to complete + cy.wait(5000); + }); +}); \ No newline at end of file diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js index d96cc4cc..a0a48718 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js @@ -38,7 +38,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo /** * Test case: Sending an invitation link from sender to recipient. */ - it('Send invitation from from ownCloud v10 to ownCloud v10', () => { + it('Send invitation from ownCloud v10 to ownCloud v10', () => { // Step 1: Log in to the sender's ownCloud instance cy.loginOwncloud(senderUrl, senderUsername, senderPassword); @@ -57,7 +57,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo /** * Test case: Accepting the invitation link on the recipient's side. */ - it('Accept invitation from from ownCloud v10 to ownCloud v10', () => { + it('Accept invitation from ownCloud v10 to ownCloud v10', () => { const expectedContactDisplayName = senderUsername; // Extract domain without protocol or trailing slash // Note: The 'reva' prefix is added to the expected contact domain as per application behavior @@ -86,7 +86,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo /** * Test case: Sharing a file via ScienceMesh from sender to recipient. */ - it('Send ScienceMesh share of a file from ownCloud v10 to ownCloud v10', () => { + it('Send ScienceMesh share of a from ownCloud v10 to ownCloud v10', () => { // Step 1: Log in to the sender's ownCloud instance cy.loginOwncloud(senderUrl, senderUsername, senderPassword); @@ -114,7 +114,7 @@ describe('Invite link federated sharing via ScienceMesh functionality for ownClo * Test Case: Receiving and accepting a ScienceMesh file share on ownCloud 2. * This test verifies that the shared file appears in the "Sharing In" section. */ - it('Receive ScienceMesh share of a file from ownCloud v10 to ownCloud v10', () => { + it('Receive ScienceMesh share of a from ownCloud v10 to ownCloud v10', () => { // Step 1: Log in to the recipient's ownCloud instance cy.loginOwncloud(recipientUrl, recipientUsername, recipientPassword); diff --git a/dev/ocm-test-suite/invite-link/nextcloud-ocis.sh b/dev/ocm-test-suite/invite-link/nextcloud-ocis.sh deleted file mode 100755 index f80c31a6..00000000 --- a/dev/ocm-test-suite/invite-link/nextcloud-ocis.sh +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# oCIS version: -# - 5.0.9 -EFSS_PLATFORM_2_VERSION=${2:-"5.0.9"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function changeInFile() { - local file_path="${1}" - local original="${2}" - local replacement="${3}" - - sed -i "s#${original}#${replacement}#g" "${file_path}" -} - -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} - -function createReva() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "creating reva for ${platform} ${number}" - - # make sure scripts are executable. - chmod +x "${ENV_ROOT}/temp/reva/run.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/kill.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/entrypoint.sh" >/dev/null 2>&1 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/temp/reva/run.sh:/usr/bin/run.sh" \ - -v "${ENV_ROOT}/temp/reva/kill.sh:/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/temp/reva/entrypoint.sh:/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad -} - -function sciencemeshInsertIntoDB() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "configuring ScienceMesh app for efss ${platform} ${number}" - - # run db injections. - mysql_cmd="docker exec "maria${platform}${number}.docker" mariadb -u root -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek efss" - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 -} - -function createEfssOcis() { - local number="${1}" - - redirect_to_null_cmd echo "creating efss ocis ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="ocis${number}.docker" \ - -e OCIS_LOG_LEVEL=info \ - -e OCIS_LOG_COLOR=true \ - -e OCIS_LOG_PRETTY=true \ - -e PROXY_HTTP_ADDR=0.0.0.0:443 \ - -e OCIS_URL="https://ocis${number}.docker" \ - -e OCIS_INSECURE=true \ - -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" \ - -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" \ - -e PROXY_ENABLE_BASIC_AUTH=true \ - -e IDM_ADMIN_PASSWORD=admin \ - -e IDM_CREATE_DEMO_USERS=true \ - -e FRONTEND_OCS_INCLUDE_OCM_SHAREES=true \ - -e FRONTEND_OCS_LIST_OCM_SHARES=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING=true \ - -e OCIS_ADD_RUN_SERVICES=ocm \ - -e OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE=/dev-stock/ocmproviders.json \ - -e GRAPH_INCLUDE_OCM_SHAREES=true \ - -e OCM_OCM_INVITE_MANAGER_INSECURE=true \ - -e OCM_OCM_SHARE_PROVIDER_INSECURE=true \ - -e OCM_OCM_STORAGE_PROVIDER_INSECURE=true \ - -e WEB_UI_CONFIG_FILE=/dev-stock/web-ui-config.json \ - -v "${ENV_ROOT}/temp/ocis:/dev-stock" \ - -v "${ENV_ROOT}/temp/certificates:/certificates" \ - -v "${ENV_ROOT}/temp/certificate-authority:/certificate-authority" \ - --entrypoint /bin/sh \ - "owncloud/ocis:5.0.9@sha256:96671605863b38b0b8021400fdb2d843586dfa31451a8c7766f15eabe85d8267" \ - -c "ocis init || true; ocis server" -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/certificates" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/configs/ocis" "${ENV_ROOT}/temp/ocis" -cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/temp/" -cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/temp/reva/configs" -cp -f "${ENV_ROOT}/docker/tls/certificates/ocis"* "${ENV_ROOT}/temp/certificates" -cp -fr "${ENV_ROOT}/docker/tls/certificate-authority" "${ENV_ROOT}/temp/certificate-authority" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-sciencemesh.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# fix permissions. -chmod -R 777 "${ENV_ROOT}/temp/certificates" -chmod -R 777 "${ENV_ROOT}/temp/certificate-authority" - -# remove unnecessary configs. -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-codimd.toml" -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-collabora.toml" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -# insert real domain names into ocmproviders.json -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--domain--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--homepage--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--path--|" "ocis1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--host--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--path--|" "ocis1.docker/dav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--host--|" "ocis1.docker" - -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--domain--|" "revanextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--homepage--|" "revanextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--|" "revanextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--path--|" "revanextcloud1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--host--|" "revanextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--|" "nextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--path--|" "nextcloud1.docker/remote.php/webdav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "nextcloud1.docker" - -############ -### oCIS ### -############ - -# syntax: -# createEfssOcis number. -# -# -# number: should be unique for each oCIS, for example: you cannot have two oCIS with same number. - -# oCISes. -createEfssOcis 1 - -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# Nextclouds. -createEfss nextcloud 1 marie radioactivity nextcloud.sh latest sciencemesh - -############ -### Reva ### -############ - -# syntax: -# createReva platform number port. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# port: maps a port on local host to port 80 of reva, for `curl` purposes! should be unique. -# for all createReva commands, if the port is not unique or is already in use by another. -# program, script would halt! - -createReva nextcloud 1 - -################### -### ScienceMesh ### -################### - -# syntax: -# sciencemeshInsertIntoDB platform number. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. - -sciencemeshInsertIntoDB nextcloud 1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://ocis1.docker -> username: einstein password: relativity" - echo "https://nextcloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/nextcloud-${P1_VER}-to-ocis-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi diff --git a/dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh b/dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh deleted file mode 100755 index d81c856c..00000000 --- a/dev/ocm-test-suite/invite-link/nextcloud-owncloud.sh +++ /dev/null @@ -1,324 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_1_VERSION=${1:-"v27.1.11"} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_2_VERSION=${2:-"v10.15.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} - -function createReva() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "creating reva for ${platform} ${number}" - - # make sure scripts are executable. - chmod +x "${ENV_ROOT}/temp/reva/run.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/kill.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/entrypoint.sh" >/dev/null 2>&1 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/temp/reva/run.sh:/usr/bin/run.sh" \ - -v "${ENV_ROOT}/temp/reva/kill.sh:/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/temp/reva/entrypoint.sh:/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad -} - -function sciencemeshInsertIntoDB() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "configuring ScienceMesh app for efss ${platform} ${number}" - - # run db injections. - mysql_cmd="docker exec "maria${platform}${number}.docker" mariadb -u root -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek efss" - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/configs" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/temp/" -cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/temp/reva/configs" -cp -f "${ENV_ROOT}/docker/scripts/ocmstub/index.js" "${ENV_ROOT}/temp/index.js" -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sciencemesh.sh" "${ENV_ROOT}/temp/owncloud.sh" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-sciencemesh.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# remove unnecessary configs. -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-codimd.toml" -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-collabora.toml" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -############ -### EFSS ### -############ - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownCloud. -createEfss owncloud 1 marie radioactivity owncloud.sh latest sciencemesh - -# Nextcloud. -createEfss nextcloud 1 einstein relativity nextcloud.sh latest sciencemesh - -############ -### Reva ### -############ - -# syntax: -# createReva platform number port. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# port: maps a port on local host to port 80 of reva, for `curl` purposes! should be unique. -# for all createReva commands, if the port is not unique or is already in use by another. -# program, script would halt! - -createReva owncloud 1 -createReva nextcloud 1 - -################### -### ScienceMesh ### -################### - -# syntax: -# sciencemeshInsertIntoDB platform number. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. - -sciencemeshInsertIntoDB owncloud 1 -sciencemeshInsertIntoDB nextcloud 1 - -###################### -### Mesh directory ### -###################### -docker run --detach --network=testnet \ - --name=meshdir.docker \ - -e HOST="meshdir" \ - -v "${ENV_ROOT}/temp/index.js:/ocmstub/index.js" \ - pondersource/dev-stock-ocmstub \ - >/dev/null 2>&1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/nextcloud-${P1_VER}-to-owncloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh index f2fa470f..91751a5e 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-sm-nextcloud-sm.sh @@ -130,8 +130,8 @@ main() { if [ "${SCRIPT_MODE}" = "dev" ]; then run_dev \ - "https://nextcloud1.docker (username: einstein, password: relativity)" \ - "https://nextcloud2.docker (username: michiel, password: dejong)" + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://nextcloud2.docker (username: michiel, password: dejong)" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh new file mode 100644 index 00000000..4ac161f4 --- /dev/null +++ b/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to oCIS OCM invite link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# specifically Nextcloud and oCIS, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. +# Usage: +# ./nextcloud-ocis.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of Nextcloud (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of oCIS (default: "5.0.9"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +# Example: +# ./nextcloud-ocis.sh v27.1.11 5.0.9 ci electron +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="5.0.9" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Configure OCM providers for oCIS + prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "revanextcloud1.docker,nextcloud1.docker,remote.php/webdav/" + + # Create EFSS containers + create_nextcloud 1 "marie" "radioactivity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_ocis 1 "${EFSS_PLATFORM_2_VERSION}" + + # Create Reva containers with disabled app configs + local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" + create_reva "nextcloud" 1 pondersource/revad latest "${disabled_configs}" + + # Configure ScienceMesh integration + configure_sciencemesh "nextcloud" 1 "https://revanextcloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + + # Start Mesh Directory + create_meshdir pondersource/ocmstub v1.0.0 + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://nextcloud1.docker (username: marie, password: radioactivity)" \ + "https://ocis1.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-owncloud-sm.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-owncloud-sm.sh new file mode 100644 index 00000000..acaef6a7 --- /dev/null +++ b/dev/ocm-test-suite/invite-link/nextcloud-sm-owncloud-sm.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test Nextcloud to ownCloud OCM invite link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# specifically Nextcloud and ownCloud, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. +# Usage: +# ./nextcloud-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of Nextcloud (default: "v27.1.11"). +# EFSS_PLATFORM_2_VERSION : Version of ownCloud (default: "v10.15.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +# Example: +# ./nextcloud-owncloud.sh v27.1.11 v10.15.0 ci electron +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v27.1.11" +DEFAULT_EFSS_2_VERSION="v10.15.0" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + # Create Reva containers with disabled app configs + local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" + create_reva "nextcloud" 1 pondersource/revad latest "${disabled_configs}" + create_reva "owncloud" 1 pondersource/revad latest "${disabled_configs}" + + # Configure ScienceMesh integration + configure_sciencemesh "nextcloud" 1 "https://revanextcloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + configure_sciencemesh "owncloud" 1 "https://revaowncloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + + # Start Mesh Directory + create_meshdir pondersource/ocmstub v1.0.0 + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://nextcloud1.docker (username: einstein, password: relativity)" \ + "https://owncloud1.docker (username: marie, password: radioactivity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh new file mode 100644 index 00000000..c3093be2 --- /dev/null +++ b/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test oCIS to Nextcloud OCM invite link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# specifically oCIS and Nextcloud, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. +# Usage: +# ./ocis-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of oCIS (default: "v5.0.9"). +# EFSS_PLATFORM_2_VERSION : Version of Nextcloud (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +# Example: +# ./ocis-nextcloud.sh v5.0.9 v27.1.11 ci electron +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v5.0.9" +DEFAULT_EFSS_2_VERSION="v27.1.11" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Configure OCM providers for oCIS + prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "revanextcloud1.docker,nextcloud1.docker,remote.php/webdav/" + + # Create EFSS containers + create_ocis 1 "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 1 "marie" "radioactivity" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + + # Create Reva containers with disabled app configs + local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" + create_reva "nextcloud" 1 pondersource/revad latest "${disabled_configs}" + + # Configure ScienceMesh integration + configure_sciencemesh "nextcloud" 1 "https://revanextcloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + + # Start Mesh Directory + create_meshdir pondersource/ocmstub v1.0.0 + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://ocis1.docker (username: einstein, password: relativity)" \ + "https://nextcloud1.docker (username: marie, password: radioactivity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/invite-link/ocis-nextcloud.sh b/dev/ocm-test-suite/invite-link/ocis-nextcloud.sh deleted file mode 100755 index b3c7b20f..00000000 --- a/dev/ocm-test-suite/invite-link/ocis-nextcloud.sh +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# oCIS version: -# - 5.0.9 -EFSS_PLATFORM_1_VERSION=${1:-"5.0.9"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_2_VERSION=${2:-"v27.1.11"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function changeInFile() { - local file_path="${1}" - local original="${2}" - local replacement="${3}" - - sed -i "s#${original}#${replacement}#g" "${file_path}" -} - -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} - -function createReva() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "creating reva for ${platform} ${number}" - - # make sure scripts are executable. - chmod +x "${ENV_ROOT}/temp/reva/run.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/kill.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/entrypoint.sh" >/dev/null 2>&1 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/temp/reva/run.sh:/usr/bin/run.sh" \ - -v "${ENV_ROOT}/temp/reva/kill.sh:/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/temp/reva/entrypoint.sh:/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad -} - -function sciencemeshInsertIntoDB() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "configuring ScienceMesh app for efss ${platform} ${number}" - - # run db injections. - mysql_cmd="docker exec "maria${platform}${number}.docker" mariadb -u root -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek efss" - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 -} - -function createEfssOcis() { - local number="${1}" - - redirect_to_null_cmd echo "creating efss ocis ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="ocis${number}.docker" \ - -e OCIS_LOG_LEVEL=info \ - -e OCIS_LOG_COLOR=true \ - -e OCIS_LOG_PRETTY=true \ - -e PROXY_HTTP_ADDR=0.0.0.0:443 \ - -e OCIS_URL="https://ocis${number}.docker" \ - -e OCIS_INSECURE=true \ - -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" \ - -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" \ - -e PROXY_ENABLE_BASIC_AUTH=true \ - -e IDM_ADMIN_PASSWORD=admin \ - -e IDM_CREATE_DEMO_USERS=true \ - -e FRONTEND_OCS_INCLUDE_OCM_SHAREES=true \ - -e FRONTEND_OCS_LIST_OCM_SHARES=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING=true \ - -e OCIS_ADD_RUN_SERVICES=ocm \ - -e OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE=/dev-stock/ocmproviders.json \ - -e GRAPH_INCLUDE_OCM_SHAREES=true \ - -e OCM_OCM_INVITE_MANAGER_INSECURE=true \ - -e OCM_OCM_SHARE_PROVIDER_INSECURE=true \ - -e OCM_OCM_STORAGE_PROVIDER_INSECURE=true \ - -e WEB_UI_CONFIG_FILE=/dev-stock/web-ui-config.json \ - -v "${ENV_ROOT}/temp/ocis:/dev-stock" \ - -v "${ENV_ROOT}/temp/certificates:/certificates" \ - -v "${ENV_ROOT}/temp/certificate-authority:/certificate-authority" \ - --entrypoint /bin/sh \ - "owncloud/ocis:5.0.9@sha256:96671605863b38b0b8021400fdb2d843586dfa31451a8c7766f15eabe85d8267" \ - -c "ocis init || true; ocis server" -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/certificates" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/configs/ocis" "${ENV_ROOT}/temp/ocis" -cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/temp/" -cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/temp/reva/configs" -cp -f "${ENV_ROOT}/docker/tls/certificates/ocis"* "${ENV_ROOT}/temp/certificates" -cp -fr "${ENV_ROOT}/docker/tls/certificate-authority" "${ENV_ROOT}/temp/certificate-authority" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-sciencemesh.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# fix permissions. -chmod -R 777 "${ENV_ROOT}/temp/certificates" -chmod -R 777 "${ENV_ROOT}/temp/certificate-authority" - -# remove unnecessary configs. -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-codimd.toml" -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-collabora.toml" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -# insert real domain names into ocmproviders.json -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--domain--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--homepage--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--path--|" "ocis1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--host--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--path--|" "ocis1.docker/dav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--host--|" "ocis1.docker" - -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--domain--|" "revanextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--homepage--|" "revanextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--|" "revanextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--path--|" "revanextcloud1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--host--|" "revanextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--|" "nextcloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--path--|" "nextcloud1.docker/remote.php/webdav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "nextcloud1.docker" - -############ -### oCIS ### -############ - -# syntax: -# createEfssOcis number. -# -# -# number: should be unique for each oCIS, for example: you cannot have two oCIS with same number. - -# oCISes. -createEfssOcis 1 - -################# -### Nextcloud ### -################# - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# init script: script for initializing efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# Nextclouds. -createEfss nextcloud 1 marie radioactivity nextcloud.sh latest sciencemesh - -############ -### Reva ### -############ - -# syntax: -# createReva platform number port. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# port: maps a port on local host to port 80 of reva, for `curl` purposes! should be unique. -# for all createReva commands, if the port is not unique or is already in use by another. -# program, script would halt! - -createReva nextcloud 1 - -################### -### ScienceMesh ### -################### - -# syntax: -# sciencemeshInsertIntoDB platform number. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. - -sciencemeshInsertIntoDB nextcloud 1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://ocis1.docker -> username: einstein password: relativity" - echo "https://nextcloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/ocis-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi diff --git a/dev/ocm-test-suite/invite-link/ocis-ocis.sh b/dev/ocm-test-suite/invite-link/ocis-ocis.sh index fded29ec..137f7a5d 100755 --- a/dev/ocm-test-suite/invite-link/ocis-ocis.sh +++ b/dev/ocm-test-suite/invite-link/ocis-ocis.sh @@ -13,12 +13,12 @@ # Usage: # ./ocis-ocis.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] # Arguments: -# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "5.0.9"). -# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "5.0.9"). +# EFSS_PLATFORM_1_VERSION : Version of the first EFSS platform (default: "v5.0.9"). +# EFSS_PLATFORM_2_VERSION : Version of the second EFSS platform (default: "v5.0.9"). # SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. # BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. # Example: -# ./ocis-ocis.sh 5.0.9 5.0.9 ci electron +# ./ocis-ocis.sh v5.0.9 v5.0.9 ci electron # ----------------------------------------------------------------------------------- # Exit immediately if a command exits with a non-zero status, @@ -30,8 +30,8 @@ set -euo pipefail # ----------------------------------------------------------------------------------- # Default versions -DEFAULT_EFSS_1_VERSION="5.0.9" -DEFAULT_EFSS_2_VERSION="5.0.9" +DEFAULT_EFSS_1_VERSION="v5.0.9" +DEFAULT_EFSS_2_VERSION="v5.0.9" # ----------------------------------------------------------------------------------- # Function: resolve_script_dir @@ -122,36 +122,15 @@ main() { initialize_environment "../../.." setup "$@" - # Prepare oCIS specific environment with appropriate configurations - # case "${TEST_SCENARIO}" in - # "ocis-ocis") - # prepare_ocis_environment \ - # "ocis1.docker,ocis1.docker,dav/" \ - # "ocis2.docker,ocis2.docker,dav/" - # ;; - # "ocis-owncloud") - # prepare_ocis_environment \ - # "ocis1.docker,ocis1.docker,dav/" \ - # "revaowncloud1.docker,owncloud1.docker,remote.php/webdav/" - # ;; - # "ocis-nextcloud") - # prepare_ocis_environment \ - # "ocis1.docker,ocis1.docker,dav/" \ - # "revanextcloud1.docker,nextcloud1.docker,remote.php/webdav/" - # ;; - # *) - # error_exit "Unknown test scenario: ${TEST_SCENARIO}" - # ;; - # esac - + # Configure OCM providers for oCIS prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "ocis2.docker,ocis2.docker,dav/" # Create EFSS containers # # id # image # tag create_ocis 1 "owncloud/ocis" "${EFSS_PLATFORM_1_VERSION}" create_ocis 2 "owncloud/ocis" "${EFSS_PLATFORM_2_VERSION}" - - if [ "${SCRIPT_MODE}" = "dev" ]; then + +if [ "${SCRIPT_MODE}" = "dev" ]; then run_dev \ "https://ocis1.docker (username: einstein, password: relativity)" \ "https://ocis2.docker (username: marie, password: radioactivity)" diff --git a/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh b/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh new file mode 100644 index 00000000..f1d36e65 --- /dev/null +++ b/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test oCIS to ownCloud OCM invite link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# specifically oCIS and ownCloud, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. +# Usage: +# ./ocis-owncloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of oCIS (default: "5.0.9"). +# EFSS_PLATFORM_2_VERSION : Version of ownCloud (default: "v10.15.0"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +# Example: +# ./ocis-owncloud.sh 5.0.9 v10.15.0 ci electron +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="5.0.9" +DEFAULT_EFSS_2_VERSION="v10.15.0" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Configure OCM providers for oCIS + prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "revaowncloud2.docker,owncloud2.docker,remote.php/webdav/" + + # Create EFSS containers + create_ocis 1 "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 2 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + + # Create Reva containers with disabled app configs + local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" + create_reva "owncloud" 2 pondersource/revad latest "${disabled_configs}" + + # Configure ScienceMesh integration + configure_sciencemesh "owncloud" 2 "https://revaowncloud2.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + + # Start Mesh Directory + create_meshdir pondersource/ocmstub v1.0.0 + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://ocis1.docker (username: einstein, password: relativity)" \ + "https://owncloud2.docker (username: marie, password: radioactivity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/invite-link/ocis-owncloud.sh b/dev/ocm-test-suite/invite-link/ocis-owncloud.sh deleted file mode 100755 index c877fd84..00000000 --- a/dev/ocm-test-suite/invite-link/ocis-owncloud.sh +++ /dev/null @@ -1,390 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# oCIS version: -# - 5.0.9 -EFSS_PLATFORM_1_VERSION=${1:-"5.0.9"} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_2_VERSION=${2:-"v10.15.0"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function changeInFile() { - local file_path="${1}" - local original="${2}" - local replacement="${3}" - - sed -i "s#${original}#${replacement}#g" "${file_path}" -} - -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} - -function createReva() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "creating reva for ${platform} ${number}" - - # make sure scripts are executable. - chmod +x "${ENV_ROOT}/temp/reva/run.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/kill.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/entrypoint.sh" >/dev/null 2>&1 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/temp/reva/run.sh:/usr/bin/run.sh" \ - -v "${ENV_ROOT}/temp/reva/kill.sh:/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/temp/reva/entrypoint.sh:/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad -} - -function sciencemeshInsertIntoDB() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "configuring ScienceMesh app for efss ${platform} ${number}" - - # run db injections. - mysql_cmd="docker exec "maria${platform}${number}.docker" mariadb -u root -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek efss" - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 -} - -function createEfssOcis() { - local number="${1}" - - redirect_to_null_cmd echo "creating efss ocis ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="ocis${number}.docker" \ - -e OCIS_LOG_LEVEL=info \ - -e OCIS_LOG_COLOR=true \ - -e OCIS_LOG_PRETTY=true \ - -e PROXY_HTTP_ADDR=0.0.0.0:443 \ - -e OCIS_URL="https://ocis${number}.docker" \ - -e OCIS_INSECURE=true \ - -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" \ - -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" \ - -e PROXY_ENABLE_BASIC_AUTH=true \ - -e IDM_ADMIN_PASSWORD=admin \ - -e IDM_CREATE_DEMO_USERS=true \ - -e FRONTEND_OCS_INCLUDE_OCM_SHAREES=true \ - -e FRONTEND_OCS_LIST_OCM_SHARES=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING=true \ - -e OCIS_ADD_RUN_SERVICES=ocm \ - -e OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE=/dev-stock/ocmproviders.json \ - -e GRAPH_INCLUDE_OCM_SHAREES=true \ - -e OCM_OCM_INVITE_MANAGER_INSECURE=true \ - -e OCM_OCM_SHARE_PROVIDER_INSECURE=true \ - -e OCM_OCM_STORAGE_PROVIDER_INSECURE=true \ - -e WEB_UI_CONFIG_FILE=/dev-stock/web-ui-config.json \ - -v "${ENV_ROOT}/temp/ocis:/dev-stock" \ - -v "${ENV_ROOT}/temp/certificates:/certificates" \ - -v "${ENV_ROOT}/temp/certificate-authority:/certificate-authority" \ - --entrypoint /bin/sh \ - "owncloud/ocis:5.0.9@sha256:96671605863b38b0b8021400fdb2d843586dfa31451a8c7766f15eabe85d8267" \ - -c "ocis init || true; ocis server" -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/certificates" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/configs/ocis" "${ENV_ROOT}/temp/ocis" -cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/temp/" -cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/temp/reva/configs" -cp -f "${ENV_ROOT}/docker/tls/certificates/ocis"* "${ENV_ROOT}/temp/certificates" -cp -fr "${ENV_ROOT}/docker/tls/certificate-authority" "${ENV_ROOT}/temp/certificate-authority" -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sciencemesh.sh" "${ENV_ROOT}/temp/owncloud.sh" - -# fix permissions. -chmod -R 777 "${ENV_ROOT}/temp/certificates" -chmod -R 777 "${ENV_ROOT}/temp/certificate-authority" - -# remove unnecessary configs. -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-codimd.toml" -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-collabora.toml" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -# insert real domain names into ocmproviders.json -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--domain--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--homepage--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--path--|" "ocis1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--host--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--path--|" "ocis1.docker/dav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--host--|" "ocis1.docker" - -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--domain--|" "revaowncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--homepage--|" "revaowncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--|" "revaowncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--path--|" "revaowncloud1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--host--|" "revaowncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--|" "owncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--path--|" "owncloud1.docker/remote.php/webdav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "owncloud1.docker" - -############ -### oCIS ### -############ - -# syntax: -# createEfssOcis number. -# -# -# number: should be unique for each oCIS, for example: you cannot have two oCIS with same number. - -# oCISes. -createEfssOcis 1 - -################ -### ownCloud ### -################ - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownClouds. -createEfss owncloud 1 marie radioactivity owncloud.sh latest sciencemesh - -############ -### Reva ### -############ - -# syntax: -# createReva platform number port. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# port: maps a port on local host to port 80 of reva, for `curl` purposes! should be unique. -# for all createReva commands, if the port is not unique or is already in use by another. -# program, script would halt! - -createReva owncloud 1 - -################### -### ScienceMesh ### -################### - -# syntax: -# sciencemeshInsertIntoDB platform number. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. - -sciencemeshInsertIntoDB owncloud 1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://ocis1.docker -> username: einstein password: relativity" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/ocis-${P1_VER}-to-owncloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi diff --git a/dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh b/dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh deleted file mode 100755 index 577b8e3c..00000000 --- a/dev/ocm-test-suite/invite-link/owncloud-nextcloud.sh +++ /dev/null @@ -1,324 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} - -# nextcloud version: -# - v27.1.11 -# - v28.0.14 -EFSS_PLATFORM_2_VERSION=${2:-"v28.0.14"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} - -function createReva() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "creating reva for ${platform} ${number}" - - # make sure scripts are executable. - chmod +x "${ENV_ROOT}/temp/reva/run.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/kill.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/entrypoint.sh" >/dev/null 2>&1 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/temp/reva/run.sh:/usr/bin/run.sh" \ - -v "${ENV_ROOT}/temp/reva/kill.sh:/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/temp/reva/entrypoint.sh:/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad -} - -function sciencemeshInsertIntoDB() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "configuring ScienceMesh app for efss ${platform} ${number}" - - # run db injections. - mysql_cmd="docker exec "maria${platform}${number}.docker" mariadb -u root -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek efss" - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/configs" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/temp/" -cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/temp/reva/configs" -cp -f "${ENV_ROOT}/docker/scripts/ocmstub/index.js" "${ENV_ROOT}/temp/index.js" -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sciencemesh.sh" "${ENV_ROOT}/temp/owncloud.sh" -cp -f "${ENV_ROOT}/docker/scripts/init/nextcloud-sciencemesh.sh" "${ENV_ROOT}/temp/nextcloud.sh" - -# remove unnecessary configs. -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-codimd.toml" -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-collabora.toml" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -############ -### EFSS ### -############ - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownCloud. -createEfss owncloud 1 marie radioactivity owncloud.sh latest sciencemesh - -# Nextcloud. -createEfss nextcloud 1 einstein relativity nextcloud.sh latest sciencemesh - -############ -### Reva ### -############ - -# syntax: -# createReva platform number port. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# port: maps a port on local host to port 80 of reva, for `curl` purposes! should be unique. -# for all createReva commands, if the port is not unique or is already in use by another. -# program, script would halt! - -createReva owncloud 1 -createReva nextcloud 1 - -################### -### ScienceMesh ### -################### - -# syntax: -# sciencemeshInsertIntoDB platform number. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. - -sciencemeshInsertIntoDB owncloud 1 -sciencemeshInsertIntoDB nextcloud 1 - -###################### -### Mesh directory ### -###################### -docker run --detach --network=testnet \ - --name=meshdir.docker \ - -e HOST="meshdir" \ - -v "${ENV_ROOT}/temp/index.js:/ocmstub/index.js" \ - pondersource/dev-stock-ocmstub \ - >/dev/null 2>&1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://nextcloud1.docker -> username: einstein password: relativity" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/owncloud-${P1_VER}-to-nextcloud-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi diff --git a/dev/ocm-test-suite/invite-link/owncloud-ocis.sh b/dev/ocm-test-suite/invite-link/owncloud-ocis.sh deleted file mode 100755 index 2b68f2c7..00000000 --- a/dev/ocm-test-suite/invite-link/owncloud-ocis.sh +++ /dev/null @@ -1,391 +0,0 @@ -#!/usr/bin/env bash - -# @michielbdejong halt on error in docker init scripts. -set -e - -# find this scripts location. -SOURCE=${BASH_SOURCE[0]} -while [ -L "${SOURCE}" ]; do # resolve "${SOURCE}" until the file is no longer a symlink. - DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - SOURCE=$(readlink "${SOURCE}") - # if "${SOURCE}" was a relative symlink, we need to resolve it relative to the path where the symlink file was located. - [[ "${SOURCE}" != /* ]] && SOURCE="${DIR}/${SOURCE}" -done -DIR=$( cd -P "$( dirname "${SOURCE}" )" >/dev/null 2>&1 && pwd ) - -cd "${DIR}/../../.." || exit - -ENV_ROOT=$(pwd) -export ENV_ROOT=${ENV_ROOT} - - -# owncloud version: -# - v10.15.0 -EFSS_PLATFORM_1_VERSION=${1:-"v10.15.0"} - -# oCIS version: -# - 5.0.9 -EFSS_PLATFORM_2_VERSION=${2:-"5.0.9"} - -# script mode: dev, ci. default is dev. -SCRIPT_MODE=${3:-"dev"} - -# browser platform: chrome, edge, firefox, electron. default is electron. -# only applies on SCRIPT_MODE=ci -BROWSER_PLATFORM=${4:-"electron"} - -function redirect_to_null_cmd() { - if [ "${SCRIPT_MODE}" = "ci" ]; then - "$@" >/dev/null 2>&1 - else - "$@" - fi -} - -function waitForPort () { - redirect_to_null_cmd echo waitForPort "${1} ${2}" - # the "| cat" after the "| grep" is to prevent the command from exiting with 1 if no match is found by grep. - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - until [ "${x}" -ne 0 ] - do - redirect_to_null_cmd echo Waiting for "${1} to open port ${2}, this usually takes about 10 seconds ... ${x}" - sleep 1 - x=$(docker exec "${1}" ss -tulpn | grep -c "${2}" | cat) - done - redirect_to_null_cmd echo "${1} port ${2} is open" -} - -function changeInFile() { - local file_path="${1}" - local original="${2}" - local replacement="${3}" - - sed -i "s#${original}#${replacement}#g" "${file_path}" -} - -function createEfss() { - local platform="${1}" - local number="${2}" - local user="${3}" - local password="${4}" - local init_script="${5}" - local tag="${6-latest}" - local image="${7}" - - if [[ -z "${image}" ]]; then - local image="pondersource/dev-stock-${platform}" - else - local image="pondersource/dev-stock-${platform}-${image}" - fi - - redirect_to_null_cmd echo "creating efss ${platform} ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="maria${platform}${number}.docker" \ - -e MARIADB_ROOT_PASSWORD=eilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek \ - mariadb:11.4.2 \ - --transaction-isolation=READ-COMMITTED \ - --binlog-format=ROW \ - --innodb-file-per-table=1 \ - --skip-innodb-read-only-compressed - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="${platform}${number}.docker" \ - --add-host "host.docker.internal:host-gateway" \ - -e HOST="${platform}${number}" \ - -e DBHOST="maria${platform}${number}.docker" \ - -e USER="${user}" \ - -e PASS="${password}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/${init_script}:/${platform}-init.sh" \ - -v "${ENV_ROOT}/docker/scripts/entrypoint.sh:/entrypoint.sh" \ - "${image}:${tag}" - - # wait for hostname port to be open. - waitForPort "maria${platform}${number}.docker" 3306 - waitForPort "${platform}${number}.docker" 443 - - # add self-signed certificates to os and trust them. (use >/dev/null 2>&1 to shut these up) - docker exec "${platform}${number}.docker" bash -c "cp -f /certificates/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /certificate-authority/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cp -f /tls/*.crt /usr/local/share/ca-certificates/ || true" >/dev/null 2>&1 - docker exec "${platform}${number}.docker" update-ca-certificates >/dev/null 2>&1 - docker exec "${platform}${number}.docker" bash -c "cat /etc/ssl/certs/ca-certificates.crt >> /var/www/html/resources/config/ca-bundle.crt" >/dev/null 2>&1 - - # run init script inside efss. - redirect_to_null_cmd docker exec -u www-data "${platform}${number}.docker" bash "/${platform}-init.sh" - - redirect_to_null_cmd echo "" -} - -function createReva() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "creating reva for ${platform} ${number}" - - # make sure scripts are executable. - chmod +x "${ENV_ROOT}/temp/reva/run.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/kill.sh" >/dev/null 2>&1 - chmod +x "${ENV_ROOT}/temp/reva/entrypoint.sh" >/dev/null 2>&1 - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="reva${platform}${number}.docker" \ - -e HOST="reva${platform}${number}" \ - -v "${ENV_ROOT}/docker/tls/certificates:/certificates" \ - -v "${ENV_ROOT}/docker/tls/certificate-authority:/certificate-authority" \ - -v "${ENV_ROOT}/temp/reva/configs:/configs/revad" \ - -v "${ENV_ROOT}/temp/reva/run.sh:/usr/bin/run.sh" \ - -v "${ENV_ROOT}/temp/reva/kill.sh:/usr/bin/kill.sh" \ - -v "${ENV_ROOT}/temp/reva/entrypoint.sh:/usr/bin/entrypoint.sh" \ - pondersource/dev-stock-revad -} - -function sciencemeshInsertIntoDB() { - local platform="${1}" - local number="${2}" - - redirect_to_null_cmd echo "configuring ScienceMesh app for efss ${platform} ${number}" - - # run db injections. - mysql_cmd="docker exec "maria${platform}${number}.docker" mariadb -u root -peilohtho9oTahsuongeeTh7reedahPo1Ohwi3aek efss" - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'iopUrl', 'https://reva${platform}${number}.docker/');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'revaSharedSecret', 'shared-secret-1');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'meshDirectoryUrl', 'https://meshdir.docker/meshdir');" >/dev/null 2>&1 - $mysql_cmd -e "insert into oc_appconfig (appid, configkey, configvalue) values ('sciencemesh', 'inviteManagerApikey', 'invite-manager-endpoint');" >/dev/null 2>&1 -} - -function createEfssOcis() { - local number="${1}" - - redirect_to_null_cmd echo "creating efss ocis ${number}" - - redirect_to_null_cmd docker run --detach --network=testnet \ - --name="ocis${number}.docker" \ - -e OCIS_LOG_LEVEL=info \ - -e OCIS_LOG_COLOR=true \ - -e OCIS_LOG_PRETTY=true \ - -e PROXY_HTTP_ADDR=0.0.0.0:443 \ - -e OCIS_URL="https://ocis${number}.docker" \ - -e OCIS_INSECURE=true \ - -e PROXY_TRANSPORT_TLS_KEY="/certificates/ocis${number}.key" \ - -e PROXY_TRANSPORT_TLS_CERT="/certificates/ocis${number}.crt" \ - -e PROXY_ENABLE_BASIC_AUTH=true \ - -e IDM_ADMIN_PASSWORD=admin \ - -e IDM_CREATE_DEMO_USERS=true \ - -e FRONTEND_OCS_INCLUDE_OCM_SHAREES=true \ - -e FRONTEND_OCS_LIST_OCM_SHARES=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING=true \ - -e FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING=true \ - -e OCIS_ADD_RUN_SERVICES=ocm \ - -e OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE=/dev-stock/ocmproviders.json \ - -e GRAPH_INCLUDE_OCM_SHAREES=true \ - -e OCM_OCM_INVITE_MANAGER_INSECURE=true \ - -e OCM_OCM_SHARE_PROVIDER_INSECURE=true \ - -e OCM_OCM_STORAGE_PROVIDER_INSECURE=true \ - -e WEB_UI_CONFIG_FILE=/dev-stock/web-ui-config.json \ - -v "${ENV_ROOT}/temp/ocis:/dev-stock" \ - -v "${ENV_ROOT}/temp/certificates:/certificates" \ - -v "${ENV_ROOT}/temp/certificate-authority:/certificate-authority" \ - --entrypoint /bin/sh \ - "owncloud/ocis:5.0.9@sha256:96671605863b38b0b8021400fdb2d843586dfa31451a8c7766f15eabe85d8267" \ - -c "ocis init || true; ocis server" -} - -# delete and create temp directory. -rm -rf "${ENV_ROOT}/temp" && mkdir -p "${ENV_ROOT}/temp/certificates" - -# copy init files. -cp -fr "${ENV_ROOT}/docker/configs/ocis" "${ENV_ROOT}/temp/ocis" -cp -fr "${ENV_ROOT}/docker/scripts/reva" "${ENV_ROOT}/temp/" -cp -fr "${ENV_ROOT}/docker/configs/revad" "${ENV_ROOT}/temp/reva/configs" -cp -f "${ENV_ROOT}/docker/tls/certificates/ocis"* "${ENV_ROOT}/temp/certificates" -cp -fr "${ENV_ROOT}/docker/tls/certificate-authority" "${ENV_ROOT}/temp/certificate-authority" -cp -f "${ENV_ROOT}/docker/scripts/init/owncloud-sciencemesh.sh" "${ENV_ROOT}/temp/owncloud.sh" - -# fix permissions. -chmod -R 777 "${ENV_ROOT}/temp/certificates" -chmod -R 777 "${ENV_ROOT}/temp/certificate-authority" - -# remove unnecessary configs. -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-codimd.toml" -rm "${ENV_ROOT}/temp/reva/configs/sciencemesh-apps-collabora.toml" - -# auto clean before starting. -"${ENV_ROOT}/scripts/clean.sh" "no" - -# make sure network exists. -docker network inspect testnet >/dev/null 2>&1 || docker network create testnet >/dev/null 2>&1 - -# insert real domain names into ocmproviders.json -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--domain--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--homepage--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--path--|" "ocis1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--ocm--host--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--|" "ocis1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--path--|" "ocis1.docker/dav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance1--webdav--host--|" "ocis1.docker" - -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--domain--|" "revaowncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--homepage--|" "revaowncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--|" "revaowncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--path--|" "revaowncloud1.docker/ocm/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--ocm--host--|" "revaowncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--|" "owncloud1.docker" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--path--|" "owncloud1.docker/remote.php/webdav/" -changeInFile "${ENV_ROOT}/temp/ocis/ocmproviders.json" "|--instance2--webdav--host--|" "owncloud1.docker" - -############ -### oCIS ### -############ - -# syntax: -# createEfssOcis number. -# -# -# number: should be unique for each oCIS, for example: you cannot have two oCIS with same number. - -# oCISes. -createEfssOcis 1 - -################ -### ownCloud ### -################ - -# syntax: -# createEfss platform number username password image. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# username: username for sign in into efss. -# password: password for sign in into efss. -# tag: tag for the image, use latest if not sure. -# image: which image variation to use for container. - -# ownClouds. -createEfss owncloud 1 marie radioactivity owncloud.sh latest sciencemesh - -############ -### Reva ### -############ - -# syntax: -# createReva platform number port. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. -# port: maps a port on local host to port 80 of reva, for `curl` purposes! should be unique. -# for all createReva commands, if the port is not unique or is already in use by another. -# program, script would halt! - -createReva owncloud 1 - -################### -### ScienceMesh ### -################### - -# syntax: -# sciencemeshInsertIntoDB platform number. -# -# -# platform: owncloud, nextcloud. -# number: should be unique for each platform, for example: you cannot have two Nextclouds with same number. - -sciencemeshInsertIntoDB owncloud 1 - -if [ "${SCRIPT_MODE}" = "dev" ]; then - ############### - ### Firefox ### - ############### - - docker run --detach --network=testnet \ - --name=firefox \ - -p 5800:5800 \ - --shm-size 2g \ - -e USER_ID="${UID}" \ - -e GROUP_ID="${UID}" \ - -e DARK_MODE=1 \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert9.db:/config/profile/cert9.db:rw" \ - -v "${ENV_ROOT}/docker/tls/browsers/firefox/cert_override.txt:/config/profile/cert_override.txt:rw" \ - jlesage/firefox:latest \ - >/dev/null 2>&1 - - ################## - ### VNC Server ### - ################## - - # remove previous x11 unix socket file, avoid any problems while mounting new one. - sudo rm -rf "${ENV_ROOT}/temp/.X11-unix" - - # try to change DISPLAY_WIDTH, DISPLAY_HEIGHT to make it fit in your screen, - # NOTE: please do not commit any change related to resolution. - docker run --detach --network=testnet \ - --name=vnc-server \ - -p 5700:8080 \ - -e RUN_XTERM=no \ - -e DISPLAY_WIDTH=1920 \ - -e DISPLAY_HEIGHT=1080 \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - theasp/novnc:latest - - ############### - ### Cypress ### - ############### - - # create cypress and attach its display to the VNC server container. - # this way you can view inside cypress container through vnc server. - docker run --detach --network=testnet \ - --name="cypress.docker" \ - -e DISPLAY=vnc-server:0.0 \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -v "${ENV_ROOT}/temp/.X11-unix:/tmp/.X11-unix" \ - -w /ocm \ - --entrypoint cypress \ - cypress/included:13.13.1 \ - open --project . - - # print instructions. - clear - echo "Now browse to :" - echo "Cypress inside VNC Server -> http://localhost:5700/vnc.html, scale VNC to get to the Continue button, and run the appropriate test from ./cypress/ocm-test-suite/cypress/e2e/" - echo "Embedded Firefox -> http://localhost:5800" - echo "" - echo "Inside Embedded Firefox browse to EFSS hostname and enter the related credentials:" - echo "https://ocis1.docker -> username: einstein password: relativity" - echo "https://owncloud1.docker -> username: marie password: radioactivity" -else - # only record when testing on electron. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: true,.*/video: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: true,.*/videoCompression: false,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - ################## - ### Cypress CI ### - ################## - - # extract version up until first dot . , example: v27.1.17 becomes v27 - P1_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_1_VERSION}" )" - P2_VER="$( cut -d '.' -f 1 <<< "${EFSS_PLATFORM_2_VERSION}" )" - - # run Cypress test suite headlessly and with the defined browser. - docker run --network=testnet \ - --name="cypress.docker" \ - -v "${ENV_ROOT}/cypress/ocm-test-suite:/ocm" \ - -w /ocm \ - cypress/included:13.13.1 cypress run \ - --browser "${BROWSER_PLATFORM}" \ - --spec "cypress/e2e/invite-link/owncloud-${P1_VER}-to-ocis-${P2_VER}.cy.js" - - # revert config file back to normal. - if [ "${BROWSER_PLATFORM}" != "electron" ]; then - sed -i 's/.*video: false,.*/ video: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - sed -i 's/.*videoCompression: false,.*/ videoCompression: true,/' "${ENV_ROOT}/cypress/ocm-test-suite/cypress.config.js" - fi - - # auto clean after running tests in ci mode. do not clear terminal. - "${ENV_ROOT}/scripts/clean.sh" "no" -fi diff --git a/dev/ocm-test-suite/invite-link/owncloud-sm-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/owncloud-sm-nextcloud-sm.sh new file mode 100644 index 00000000..7f04830f --- /dev/null +++ b/dev/ocm-test-suite/invite-link/owncloud-sm-nextcloud-sm.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test ownCloud to Nextcloud OCM invite link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# specifically ownCloud and Nextcloud, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. +# Usage: +# ./owncloud-nextcloud.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of ownCloud (default: "v10.15.0"). +# EFSS_PLATFORM_2_VERSION : Version of Nextcloud (default: "v27.1.11"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +# Example: +# ./owncloud-nextcloud.sh v10.15.0 v27.1.11 ci electron +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v10.15.0" +DEFAULT_EFSS_2_VERSION="v27.1.11" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Create EFSS containers + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 1 "einstein" "relativity" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + + # Create Reva containers with disabled app configs + local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" + create_reva "owncloud" 1 pondersource/revad latest "${disabled_configs}" + create_reva "nextcloud" 1 pondersource/revad latest "${disabled_configs}" + + # Configure ScienceMesh integration + configure_sciencemesh "owncloud" 1 "https://revaowncloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + configure_sciencemesh "nextcloud" 1 "https://revanextcloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + + # Start Mesh Directory + create_meshdir pondersource/ocmstub v1.0.0 + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://owncloud1.docker (username: marie, password: radioactivity)" \ + "https://nextcloud1.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" diff --git a/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh b/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh new file mode 100644 index 00000000..0fad15ce --- /dev/null +++ b/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------------- +# Script to Test ownCloud to oCIS OCM invite link flow tests. +# Author: Mohammad Mahdi Baghbani Pourvahid +# ----------------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------------- +# Description: +# This script automates the setup and testing of EFSS (Enterprise File Synchronization and Sharing) platforms +# specifically ownCloud and oCIS, using ScienceMesh integration and tools like Reva, Cypress, and Docker containers. +# It supports both development and CI environments, with optional browser support. +# Usage: +# ./owncloud-ocis.sh [EFSS_PLATFORM_1_VERSION] [EFSS_PLATFORM_2_VERSION] [SCRIPT_MODE] [BROWSER_PLATFORM] +# Arguments: +# EFSS_PLATFORM_1_VERSION : Version of ownCloud (default: "v10.15.0"). +# EFSS_PLATFORM_2_VERSION : Version of oCIS (default: "5.0.9"). +# SCRIPT_MODE : Script mode (default: "dev"). Options: dev, ci. +# BROWSER_PLATFORM : Browser platform (default: "electron"). Options: chrome, edge, firefox, electron. +# Example: +# ./owncloud-ocis.sh v10.15.0 5.0.9 ci electron +# ----------------------------------------------------------------------------------- + +# Exit immediately if a command exits with a non-zero status, +# a variable is used but not defined, or a command in a pipeline fails +set -euo pipefail + +# ----------------------------------------------------------------------------------- +# Constants and Default Values +# ----------------------------------------------------------------------------------- + +# Default versions +DEFAULT_EFSS_1_VERSION="v10.15.0" +DEFAULT_EFSS_2_VERSION="5.0.9" + +# ----------------------------------------------------------------------------------- +# Function: resolve_script_dir +# Purpose : Resolves the absolute path of the script's directory, handling symlinks. +# Returns : +# Exports SOURCE, SCRIPT_DIR +# Note : This function relies on BASH_SOURCE, so it must be used in a Bash shell. +# ----------------------------------------------------------------------------------- +resolve_script_dir() { + local source="${BASH_SOURCE[0]}" + + # Follow symbolic links until we get the real file location + while [ -L "${source}" ]; do + # Get the directory path where the symlink is located + dir="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + # Use readlink to get the target the symlink points to + source="$(readlink "${source}")" + # If the source was a relative symlink, convert it to an absolute path + [[ "${source}" != /* ]] && source="${dir}/${source}" + done + + # After resolving symlinks, retrieve the directory of the final source + SCRIPT_DIR="$(cd -P "$(dirname "${source}")" >/dev/null 2>&1 && pwd)" + + # Exports + export SOURCE="${source}" + export SCRIPT_DIR="${SCRIPT_DIR}" +} + +# ----------------------------------------------------------------------------------- +# Function: initialize_environment +# Purpose : +# 1) Resolve the script's directory. +# 2) Change into that directory plus an optional subdirectory (if provided). +# 3) Export ENV_ROOT as the new working directory. +# 4) Source a utility script (`utils.sh`) with optional version parameters. +# +# Arguments: +# 1) $1 - Relative or absolute path to a subdirectory (optional). +# If omitted or empty, defaults to '.' (the same directory as resolve_script_dir). +# +# Usage Example: +# initialize_environment # Uses the script's directory +# initialize_environment "dev" # Changes to script's directory + "/dev" +# ----------------------------------------------------------------------------------- +initialize_environment() { + # Resolve script's directory + resolve_script_dir + + # Local variables + local subdir + # Check if a subdirectory argument was passed; default to '.' if not + subdir="${1:-.}" + + # Attempt to change into the resolved directory + the subdirectory + if cd "${SCRIPT_DIR}/${subdir}"; then + ENV_ROOT="$(pwd)" + export ENV_ROOT + else + printf "Error: %s\n" "Failed to change directory to '${SCRIPT_DIR}/${subdir}'." >&2 && exit 1 + fi + + # shellcheck source=/dev/null + # Source utility script (assuming it exists and is required for subsequent commands) + if [[ -f "${ENV_ROOT}/scripts/utils.sh" ]]; then + source "${ENV_ROOT}/scripts/utils.sh" "${DEFAULT_EFSS_1_VERSION}" "${DEFAULT_EFSS_2_VERSION}" + else + printf "Error: %s\n" "Could not source '${ENV_ROOT}/scripts/utils.sh' (file not found)." >&2 && exit 1 + fi +} + +# ----------------------------------------------------------------------------------- +# Main Execution +# ----------------------------------------------------------------------------------- + +main() { + # Initialize environment and parse arguments + initialize_environment "../../.." + setup "$@" + + # Configure OCM providers for oCIS + prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "revaowncloud1.docker,owncloud1.docker,remote.php/webdav/" + + # Create EFSS containers + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_ocis 2 "${EFSS_PLATFORM_2_VERSION}" + + # Create Reva containers with disabled app configs + local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" + create_reva "owncloud" 1 pondersource/revad latest "${disabled_configs}" + + # Configure ScienceMesh integration + configure_sciencemesh "owncloud" 1 "https://revaowncloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + + # Start Mesh Directory + create_meshdir pondersource/ocmstub v1.0.0 + + if [ "${SCRIPT_MODE}" = "dev" ]; then + run_dev \ + "https://owncloud1.docker (username: marie, password: radioactivity)" \ + "https://ocis2.docker (username: einstein, password: relativity)" + else + run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" + fi +} + +# ----------------------------------------------------------------------------------- +# Execute the main function with passed arguments +# ----------------------------------------------------------------------------------- +main "$@" From 3b2fdc87793c230b559fccd3ca3380ba5799086c Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 28 Jan 2025 20:44:39 +0000 Subject: [PATCH 177/184] fix: permissions --- dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh | 0 dev/ocm-test-suite/invite-link/nextcloud-sm-owncloud-sm.sh | 0 dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh | 0 dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh | 0 dev/ocm-test-suite/invite-link/owncloud-sm-nextcloud-sm.sh | 0 dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh | 0 6 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh mode change 100644 => 100755 dev/ocm-test-suite/invite-link/nextcloud-sm-owncloud-sm.sh mode change 100644 => 100755 dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh mode change 100644 => 100755 dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh mode change 100644 => 100755 dev/ocm-test-suite/invite-link/owncloud-sm-nextcloud-sm.sh mode change 100644 => 100755 dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh old mode 100644 new mode 100755 diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-owncloud-sm.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-owncloud-sm.sh old mode 100644 new mode 100755 diff --git a/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh old mode 100644 new mode 100755 diff --git a/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh b/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh old mode 100644 new mode 100755 diff --git a/dev/ocm-test-suite/invite-link/owncloud-sm-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/owncloud-sm-nextcloud-sm.sh old mode 100644 new mode 100755 diff --git a/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh b/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh old mode 100644 new mode 100755 From 4e193bc87c429699d5faef7c2961dddc762c81d5 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Tue, 28 Jan 2025 21:57:56 +0000 Subject: [PATCH 178/184] fix: remaining problems of the refactoring and passing tests --- .../invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js | 11 ++++++----- .../owncloud-sm-v10-to-nextcloud-sm-v27.cy.js | 1 - .../invite-link/owncloud-sm-v10-to-ocis-v5.cy.js | 1 + .../ocm-test-suite/cypress/e2e/utils/owncloud.js | 14 ++++++++++---- .../invite-link/nextcloud-sm-ocis.sh | 2 +- .../invite-link/ocis-nextcloud-sm.sh | 6 +++--- dev/ocm-test-suite/invite-link/ocis-ocis.sh | 6 +++--- dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh | 12 ++++++------ dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh | 6 +++--- 9 files changed, 33 insertions(+), 26 deletions(-) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js index 508b61fa..450e71f6 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js @@ -8,16 +8,17 @@ */ import { - createInviteTokenV27, + createInviteToken, createScienceMeshShareV27, renameFileV27, ensureFileExistsV27, } from '../utils/nextcloud-v27'; import { - openFilesAppV5, acceptInviteLinkV5, verifyFederatedContactV5, + acceptShareV5, + verifyShareV5 } from '../utils/ocis-5'; describe('Invite link federated sharing via ScienceMesh functionality between Nextcloud and oCIS', () => { @@ -57,7 +58,7 @@ describe('Invite link federated sharing via ScienceMesh functionality between Ne cy.visit(`${senderUrl}/index.php/apps/sciencemesh/contacts`); // Step 3: Generate the invite token and save it to a file - createInviteTokenV27().then((inviteToken) => { + createInviteToken().then((inviteToken) => { // Ensure the invite token is not empty expect(inviteToken).to.be.a('string').and.not.be.empty; // Save the invite token to a file for later use @@ -119,8 +120,8 @@ describe('Invite link federated sharing via ScienceMesh functionality between Ne // Step 5: Create the share for the recipient createScienceMeshShareV27( senderDomain, - recipientUsername, - recipientDomain, + recipientDisplayName, + recipientUrl, sharedFileName ); }); diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js index 0648d706..6aa3c23b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js @@ -9,7 +9,6 @@ import { createInviteLink, - acceptScienceMeshInvitation, createScienceMeshShare, renameFile, ensureFileExists, diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js index 0d521b34..0df659e4 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js @@ -8,6 +8,7 @@ */ import { + createInviteToken, createScienceMeshShare, renameFile, ensureFileExists, diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js index b38b041b..05514855 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js @@ -401,10 +401,16 @@ export function triggerActionInFileMenu(fileName, actionId) { export function triggerActionForFile(fileName, actionId) { // Find the actions container for the file and click the desired action getActionsForFile(fileName) - .find(`[data-action="${actionId}"]`) - .should('be.visible') - .as('btn') - .click(); + .should('exist') + .and('be.visible') + .within(() => { + // Find the action button and ensure it's properly loaded + cy.get(`*[data-action="${actionId}"]`) + .should('exist') + .and('be.visible') + .and('not.be.disabled') + .click({ force: true }); + }); } /** diff --git a/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh b/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh index 4ac161f4..1c6a4c3b 100755 --- a/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh +++ b/dev/ocm-test-suite/invite-link/nextcloud-sm-ocis.sh @@ -117,7 +117,7 @@ main() { # Create EFSS containers create_nextcloud 1 "marie" "radioactivity" pondersource/nextcloud "${EFSS_PLATFORM_1_VERSION}" - create_ocis 1 "${EFSS_PLATFORM_2_VERSION}" + create_ocis 1 owncloud/ocis "${EFSS_PLATFORM_2_VERSION}" # Create Reva containers with disabled app configs local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" diff --git a/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh b/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh index c3093be2..8e34de30 100755 --- a/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh +++ b/dev/ocm-test-suite/invite-link/ocis-nextcloud-sm.sh @@ -116,8 +116,8 @@ main() { prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "revanextcloud1.docker,nextcloud1.docker,remote.php/webdav/" # Create EFSS containers - create_ocis 1 "${EFSS_PLATFORM_1_VERSION}" - create_nextcloud 1 "marie" "radioactivity" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" + create_ocis 1 owncloud/ocis "${EFSS_PLATFORM_1_VERSION}" + create_nextcloud 1 "michiel" "dejong" pondersource/nextcloud "${EFSS_PLATFORM_2_VERSION}" # Create Reva containers with disabled app configs local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" @@ -132,7 +132,7 @@ main() { if [ "${SCRIPT_MODE}" = "dev" ]; then run_dev \ "https://ocis1.docker (username: einstein, password: relativity)" \ - "https://nextcloud1.docker (username: marie, password: radioactivity)" + "https://nextcloud1.docker (username: michiel, password: dejong)" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi diff --git a/dev/ocm-test-suite/invite-link/ocis-ocis.sh b/dev/ocm-test-suite/invite-link/ocis-ocis.sh index 137f7a5d..1407646d 100755 --- a/dev/ocm-test-suite/invite-link/ocis-ocis.sh +++ b/dev/ocm-test-suite/invite-link/ocis-ocis.sh @@ -126,9 +126,9 @@ main() { prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "ocis2.docker,ocis2.docker,dav/" # Create EFSS containers - # # id # image # tag - create_ocis 1 "owncloud/ocis" "${EFSS_PLATFORM_1_VERSION}" - create_ocis 2 "owncloud/ocis" "${EFSS_PLATFORM_2_VERSION}" + # # id # image # tag + create_ocis 1 owncloud/ocis "${EFSS_PLATFORM_1_VERSION}" + create_ocis 2 owncloud/ocis "${EFSS_PLATFORM_2_VERSION}" if [ "${SCRIPT_MODE}" = "dev" ]; then run_dev \ diff --git a/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh b/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh index f1d36e65..4d41c1de 100755 --- a/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh +++ b/dev/ocm-test-suite/invite-link/ocis-owncloud-sm.sh @@ -113,18 +113,18 @@ main() { setup "$@" # Configure OCM providers for oCIS - prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "revaowncloud2.docker,owncloud2.docker,remote.php/webdav/" + prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "revaowncloud1.docker,owncloud1.docker,remote.php/webdav/" # Create EFSS containers - create_ocis 1 "${EFSS_PLATFORM_1_VERSION}" - create_owncloud 2 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" + create_ocis 1 owncloud/ocis "${EFSS_PLATFORM_1_VERSION}" + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_2_VERSION}" # Create Reva containers with disabled app configs local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" - create_reva "owncloud" 2 pondersource/revad latest "${disabled_configs}" + create_reva "owncloud" 1 pondersource/revad latest "${disabled_configs}" # Configure ScienceMesh integration - configure_sciencemesh "owncloud" 2 "https://revaowncloud2.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" + configure_sciencemesh "owncloud" 1 "https://revaowncloud1.docker/" "shared-secret-1" "https://meshdir.docker/meshdir" "invite-manager-endpoint" # Start Mesh Directory create_meshdir pondersource/ocmstub v1.0.0 @@ -132,7 +132,7 @@ main() { if [ "${SCRIPT_MODE}" = "dev" ]; then run_dev \ "https://ocis1.docker (username: einstein, password: relativity)" \ - "https://owncloud2.docker (username: marie, password: radioactivity)" + "https://owncloud1.docker (username: marie, password: radioactivity)" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi diff --git a/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh b/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh index 0fad15ce..c0d1f335 100755 --- a/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh +++ b/dev/ocm-test-suite/invite-link/owncloud-sm-ocis.sh @@ -116,8 +116,8 @@ main() { prepare_ocis_environment "ocis1.docker,ocis1.docker,dav/" "revaowncloud1.docker,owncloud1.docker,remote.php/webdav/" # Create EFSS containers - create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" - create_ocis 2 "${EFSS_PLATFORM_2_VERSION}" + create_owncloud 1 "marie" "radioactivity" pondersource/owncloud "${EFSS_PLATFORM_1_VERSION}" + create_ocis 1 owncloud/ocis "${EFSS_PLATFORM_2_VERSION}" # Create Reva containers with disabled app configs local disabled_configs="sciencemesh-apps-codimd.toml sciencemesh-apps-collabora.toml" @@ -132,7 +132,7 @@ main() { if [ "${SCRIPT_MODE}" = "dev" ]; then run_dev \ "https://owncloud1.docker (username: marie, password: radioactivity)" \ - "https://ocis2.docker (username: einstein, password: relativity)" + "https://ocis1.docker (username: einstein, password: relativity)" else run_ci "${TEST_SCENARIO}" "${EFSS_PLATFORM_1}" "${EFSS_PLATFORM_2}" fi From 16977ab238b9fa79f09982186e1d9ed26ed39268 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 29 Jan 2025 10:23:33 +0330 Subject: [PATCH 179/184] remove: sciencemesh docker --- .../legacy/owncloud-sciencemesh.Dockerfile | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 docker/dockerfiles/legacy/owncloud-sciencemesh.Dockerfile diff --git a/docker/dockerfiles/legacy/owncloud-sciencemesh.Dockerfile b/docker/dockerfiles/legacy/owncloud-sciencemesh.Dockerfile deleted file mode 100644 index 1e66ffd3..00000000 --- a/docker/dockerfiles/legacy/owncloud-sciencemesh.Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM pondersource/owncloud:latest - -# keys for oci taken from: -# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys -LABEL org.opencontainers.image.licenses=MIT -LABEL org.opencontainers.image.title="PonderSource ownCloud Sciencemesh Image" -LABEL org.opencontainers.image.source="https://github.com/pondersource/dev-stock" -LABEL org.opencontainers.image.authors="Mohammad Mahdi Baghbani Pourvahid" - -USER www-data - -ARG REPO_SCIENCEMESH=https://github.com/sciencemesh/nc-sciencemesh -ARG BRANCH_SCIENCEMESH=owncloud -# CACHEBUST forces docker to clone fresh source codes from git. -# example: docker build -t your-image --build-arg CACHEBUST="default" . -# $RANDOM returns random number each time. -ARG CACHEBUST="default" -RUN git clone \ - --depth 1 \ - --branch ${BRANCH_SCIENCEMESH} \ - ${REPO_SCIENCEMESH} \ - apps/sciencemesh - -RUN cd apps/sciencemesh && git pull -RUN cd apps/sciencemesh && make - -# this file can be overrided in docker run or docker compose.yaml. -# example: docker run --volume new-init.sh:/init.sh:ro -COPY ./scripts/init/owncloud-sciencemesh.sh /init.sh - -USER root From 4d0365c685462e666e37f68aea8da436b2a603ec Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 29 Jan 2025 11:32:16 +0330 Subject: [PATCH 180/184] add: oc version to utils file --- .../cypress/e2e/deprecated/9-owncloud-to-owncloud-group.js | 2 +- .../e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js | 2 +- .../cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js | 2 +- .../e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js | 2 +- .../cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js | 2 +- .../e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js | 2 +- .../cypress/e2e/share-link/nextcloud-v27-to-owncloud-v10.cy.js | 2 +- .../cypress/e2e/share-link/nextcloud-v28-to-owncloud-v10.cy.js | 2 +- .../cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js | 2 +- .../cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js | 2 +- .../cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js | 2 +- .../cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js | 2 +- .../cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js | 2 +- .../cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js | 2 +- .../cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js | 2 +- .../cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js | 2 +- .../cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js | 2 +- .../cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js | 2 +- .../cypress/e2e/utils/{owncloud.js => owncloud-v10.js} | 0 19 files changed, 18 insertions(+), 18 deletions(-) rename cypress/ocm-test-suite/cypress/e2e/utils/{owncloud.js => owncloud-v10.js} (100%) diff --git a/cypress/ocm-test-suite/cypress/e2e/deprecated/9-owncloud-to-owncloud-group.js b/cypress/ocm-test-suite/cypress/e2e/deprecated/9-owncloud-to-owncloud-group.js index 6feb9112..3757c534 100644 --- a/cypress/ocm-test-suite/cypress/e2e/deprecated/9-owncloud-to-owncloud-group.js +++ b/cypress/ocm-test-suite/cypress/e2e/deprecated/9-owncloud-to-owncloud-group.js @@ -1,4 +1,4 @@ -import { createShareGroup, renameFile } from '../utils/owncloud' +import { createShareGroup, renameFile } from '../utils/owncloud-v10' before(() => { // makes custom commands available to all subsequent cy.origin('url') diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js index 5e125786..ede300a6 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-owncloud-sm-v10.cy.js @@ -20,7 +20,7 @@ import { acceptScienceMeshInvitation, ensureFileExists, selectAppFromLeftSide, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('Invite link federated sharing via ScienceMesh functionality between Nextcloud and ownCloud', () => { // Shared variables to avoid repetition and improve maintainability diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js index 4fb795b4..0e906eb5 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js @@ -21,7 +21,7 @@ import { acceptScienceMeshInvitation, ensureFileExists, selectAppFromLeftSide, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('Invite link federated sharing via ScienceMesh functionality between oCIS and ownCloud', () => { // Shared variables to avoid repetition and improve maintainability diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js index 6aa3c23b..7cc75c1f 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-nextcloud-sm-v27.cy.js @@ -12,7 +12,7 @@ import { createScienceMeshShare, renameFile, ensureFileExists, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; import { acceptShareV27, diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js index 0df659e4..a664d954 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js @@ -12,7 +12,7 @@ import { createScienceMeshShare, renameFile, ensureFileExists, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; import { acceptInviteLinkV5, diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js index a0a48718..000c931b 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-owncloud-sm-v10.cy.js @@ -16,7 +16,7 @@ import { renameFile, ensureFileExists, selectAppFromLeftSide, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('Invite link federated sharing via ScienceMesh functionality for ownCloud', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-owncloud-v10.cy.js index ed8e87c7..b318ddfc 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v27-to-owncloud-v10.cy.js @@ -5,7 +5,7 @@ import { import { selectAppFromLeftSide -} from '../utils/owncloud' +} from '../utils/owncloud-v10' describe('Share link federated sharing functionality for Nextcloud', () => { it('Send federated share from Nextcloud v27 to ownCloud v10', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v28-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v28-to-owncloud-v10.cy.js index b207a30b..ba62e735 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v28-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/nextcloud-v28-to-owncloud-v10.cy.js @@ -5,7 +5,7 @@ import { import { selectAppFromLeftSide -} from '../utils/owncloud' +} from '../utils/owncloud-v10' describe('Share link federated sharing functionality for Nextcloud', () => { it('Send federated share from Nextcloud v28 to ownCloud v10', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js index ce08b5ee..0dfe2c52 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v27.cy.js @@ -1,7 +1,7 @@ import { createShareLink, renameFile -} from '../utils/owncloud' +} from '../utils/owncloud-v10' import { navigationSwitchLeftSideV27, diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js index 041ce36b..964f744c 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-nextcloud-v28.cy.js @@ -1,4 +1,4 @@ -import { createShareLink, renameFile } from '../utils/owncloud' +import { createShareLink, renameFile } from '../utils/owncloud-v10' describe('Share link federated sharing functionality for ownCloud', () => { it('Send federated share from ownCloud v10 to Nextcloud v28', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js index 8af95359..66672a5c 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-link/owncloud-v10-to-owncloud-v10.cy.js @@ -2,7 +2,7 @@ import { createShareLink, renameFile, selectAppFromLeftSide -} from '../utils/owncloud' +} from '../utils/owncloud-v10' describe('Share link federated sharing functionality for ownCloud', () => { it('Send federated share from ownCloud v10 to ownCloud v10', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js index 308b1f77..0a41c23d 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v27-to-owncloud-v10.cy.js @@ -16,7 +16,7 @@ import { acceptShare, ensureFileExists, selectAppFromLeftSide, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('Native Federated Sharing Functionality for Nextcloud to ownCloud', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js index 23de636d..b2e8892d 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/nextcloud-v28-to-owncloud-v10.cy.js @@ -16,7 +16,7 @@ import { acceptShare, ensureFileExists, selectAppFromLeftSide, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('Native Federated Sharing Functionality for Nextcloud to ownCloud', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js index cfb2feb7..d5db30a3 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/ocmstub-v1-to-owncloud-v10.cy.js @@ -11,7 +11,7 @@ import { acceptShare, ensureFileExists, selectAppFromLeftSide, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('Federated sharing functionality from OcmStub to ownCloud', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js index b3554947..fe179336 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v27.cy.js @@ -17,7 +17,7 @@ import { createShare, renameFile, ensureFileExists, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('OCM federated sharing functionality for ownCloud', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js index 907bab47..d9acfbe7 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-nextcloud-v28.cy.js @@ -15,7 +15,7 @@ import { createShare, renameFile, ensureFileExists, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('OCM federated sharing functionality for ownCloud', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js index e656098e..4f9f1f16 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-ocmstub-v1.cy.js @@ -10,7 +10,7 @@ import { createShare, renameFile, ensureFileExists, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; import { generateShareAssertions, diff --git a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js index 48c6d663..011b4670 100644 --- a/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/share-with/owncloud-v10-to-owncloud-v10.cy.js @@ -12,7 +12,7 @@ import { renameFile, ensureFileExists, selectAppFromLeftSide, -} from '../utils/owncloud'; +} from '../utils/owncloud-v10'; describe('Native federated sharing functionality for ownCloud', () => { diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js b/cypress/ocm-test-suite/cypress/e2e/utils/owncloud-v10.js similarity index 100% rename from cypress/ocm-test-suite/cypress/e2e/utils/owncloud.js rename to cypress/ocm-test-suite/cypress/e2e/utils/owncloud-v10.js From 37913fe55abaa6439720c35f587427223c0eac90 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 29 Jan 2025 08:05:17 +0000 Subject: [PATCH 181/184] add: docker pull script for oc sm --- docker/pull/ocm-test-suite/owncloud-sm.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 docker/pull/ocm-test-suite/owncloud-sm.sh diff --git a/docker/pull/ocm-test-suite/owncloud-sm.sh b/docker/pull/ocm-test-suite/owncloud-sm.sh new file mode 100755 index 00000000..caf0732a --- /dev/null +++ b/docker/pull/ocm-test-suite/owncloud-sm.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# @michielbdejong halt on error in docker init scripts. +set -e + +# owncloud version: +# - 10.15.0 +EFSS_PLATFORM_VERSION=${1:-"v10.15.0"} + +# 3rd party images. +docker pull mariadb:11.4.2 +docker pull cypress/included:13.13.1 + +# dev-stock images. +docker pull "pondersource/owncloud:${EFSS_PLATFORM_VERSION}" From ed8483a2a38f2c43d17b785790abc349fb71eb41 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 29 Jan 2025 08:22:07 +0000 Subject: [PATCH 182/184] add: sm to docker push --- docker/push/all.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/push/all.sh b/docker/push/all.sh index f28213e9..781e3f6a 100755 --- a/docker/push/all.sh +++ b/docker/push/all.sh @@ -85,6 +85,8 @@ for version in "${nextcloud_versions[@]}"; do run_quietly_if_ci docker push "pondersource/nextcloud:${version}" done +run_quietly_if_ci docker push pondersource/nextcloud:v27.1.11 + # ownCloud: push multiple versions of the ownCloud Docker image. run_quietly_if_ci docker push pondersource/owncloud-base:latest owncloud_versions=("latest" "v10.15.0") @@ -92,6 +94,8 @@ for version in "${owncloud_versions[@]}"; do run_quietly_if_ci docker push "pondersource/owncloud:${version}" done +run_quietly_if_ci docker push pondersource/owncloud:v10.15.0-sm + # ----------------------------------------------------------------------------------- # End of Docker Push # ----------------------------------------------------------------------------------- From cc03811d4f1bbf2c1583cae1196585ee0ee262e3 Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Wed, 29 Jan 2025 12:04:11 +0330 Subject: [PATCH 183/184] [no ci] refactor: ocis utils name --- .../cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js | 2 +- .../cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js | 2 +- .../cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js | 2 +- .../cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js | 2 +- .../cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js | 2 +- .../ocm-test-suite/cypress/e2e/utils/{ocis-5.js => ocis-v5.js} | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename cypress/ocm-test-suite/cypress/e2e/utils/{ocis-5.js => ocis-v5.js} (99%) diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js index 450e71f6..619ef339 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/nextcloud-sm-v27-to-ocis-v5.cy.js @@ -19,7 +19,7 @@ import { verifyFederatedContactV5, acceptShareV5, verifyShareV5 -} from '../utils/ocis-5'; +} from '../utils/ocis-v5'; describe('Invite link federated sharing via ScienceMesh functionality between Nextcloud and oCIS', () => { // Shared variables to avoid repetition and improve maintainability diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js index 44f17f5b..ae2dba0d 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-nextcloud-sm-v27.cy.js @@ -13,7 +13,7 @@ import { createLegacyInviteLinkV5, createTextFileV5, createShareV5, -} from '../utils/ocis-5'; +} from '../utils/ocis-v5'; import { acceptShareV27, diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js index 90155596..6506801f 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-ocis-v5.cy.js @@ -17,7 +17,7 @@ import { createShareV5, acceptShareV5, verifyShareV5 -} from '../utils/ocis-5' +} from '../utils/ocis-v5' describe('Invite link federated sharing via ScienceMesh functionality for oCIS', () => { // Shared variables to avoid repetition and improve maintainability diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js index 0e906eb5..8459b692 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/ocis-v5-to-owncloud-sm-v10.cy.js @@ -13,7 +13,7 @@ import { createLegacyInviteLinkV5, createTextFileV5, createShareV5, -} from '../utils/ocis-5' +} from '../utils/ocis-v5' import { acceptShare, diff --git a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js index a664d954..b77dec03 100644 --- a/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js +++ b/cypress/ocm-test-suite/cypress/e2e/invite-link/owncloud-sm-v10-to-ocis-v5.cy.js @@ -19,7 +19,7 @@ import { verifyFederatedContactV5, acceptShareV5, verifyShareV5 -} from '../utils/ocis-5'; +} from '../utils/ocis-v5'; describe('Invite link federated sharing via ScienceMesh functionality between ownCloud and oCIS', () => { // Shared variables to avoid repetition and improve maintainability diff --git a/cypress/ocm-test-suite/cypress/e2e/utils/ocis-5.js b/cypress/ocm-test-suite/cypress/e2e/utils/ocis-v5.js similarity index 99% rename from cypress/ocm-test-suite/cypress/e2e/utils/ocis-5.js rename to cypress/ocm-test-suite/cypress/e2e/utils/ocis-v5.js index e6d91585..e2999133 100644 --- a/cypress/ocm-test-suite/cypress/e2e/utils/ocis-5.js +++ b/cypress/ocm-test-suite/cypress/e2e/utils/ocis-v5.js @@ -281,4 +281,4 @@ export const getRowForFileV5 = (filename) => cy.get(`[data-test-resource-name="$ .parent() .parent() .parent() - .parent() + .parent() \ No newline at end of file From 0ebecb8d86014703aa3baeb20586db95e6f2e19f Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Baghbani Pourvahid Date: Wed, 29 Jan 2025 08:28:41 +0000 Subject: [PATCH 184/184] fix: typo in sciencemesh image --- docker/push/all.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/push/all.sh b/docker/push/all.sh index 781e3f6a..7e5d4eab 100755 --- a/docker/push/all.sh +++ b/docker/push/all.sh @@ -85,7 +85,7 @@ for version in "${nextcloud_versions[@]}"; do run_quietly_if_ci docker push "pondersource/nextcloud:${version}" done -run_quietly_if_ci docker push pondersource/nextcloud:v27.1.11 +run_quietly_if_ci docker push pondersource/nextcloud:v27.1.11-sm # ownCloud: push multiple versions of the ownCloud Docker image. run_quietly_if_ci docker push pondersource/owncloud-base:latest
  • ${key}
  • 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