#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" TEST_COMMAND="${TEST_COMMAND:-composer test:unit}" SOURCE_TARGET="${SOURCE_TARGET:-}" SOURCE_HOST="${SOURCE_HOST:-${CONFIG_DB_HOST:-}}" SOURCE_PORT="${SOURCE_PORT:-${CONFIG_DB_PORT:-3306}}" SOURCE_DATABASE="${SOURCE_DATABASE:-${CONFIG_DB_DATABASE:-}}" SOURCE_USER="${SOURCE_USER:-${CONFIG_DB_USER:-}}" SOURCE_PASSWORD="${SOURCE_PASSWORD:-${CONFIG_DB_PASSWORD:-}}" NETWORK_NAME="${NETWORK_NAME:-api-test-net}" DB_CONTAINER_NAME="${DB_CONTAINER_NAME:-api-test-db}" REDIS_CONTAINER_NAME="${REDIS_CONTAINER_NAME:-api-test-redis}" PHP_IMAGE="${PHP_IMAGE:-api-php-test-runner}" CLONE_DATABASE="${CLONE_DATABASE:-}" CLONE_ROOT_PASSWORD="${CLONE_ROOT_PASSWORD:-test_root_password}" SKIP_BUILD="${SKIP_BUILD:-0}" FORCE_BUILD="${FORCE_BUILD:-0}" SKIP_TESTS="${SKIP_TESTS:-0}" KEEP_CONTAINERS="${KEEP_CONTAINERS:-0}" FORCE="${FORCE:-0}" if [[ -f ".env" ]]; then # shellcheck disable=SC2046 export $(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' .env | sed 's/\r$//') SOURCE_TARGET="${SOURCE_TARGET:-${CONFIG_DB_TARGET:-}}" fi SOURCE_TARGET="${SOURCE_TARGET:-live}" SOURCE_TARGET="$(echo "${SOURCE_TARGET}" | tr '[:upper:]' '[:lower:]')" if [[ "${SOURCE_TARGET}" != "live" && "${SOURCE_TARGET}" != "debug" ]]; then SOURCE_TARGET="live" fi if [[ "${SOURCE_TARGET}" == "debug" ]]; then SOURCE_HOST="${SOURCE_HOST:-${CONFIG_DB_DEBUG_HOST:-${CONFIG_DB_HOST:-}}}" SOURCE_DATABASE="${SOURCE_DATABASE:-${CONFIG_DB_DEBUG_DATABASE:-${CONFIG_DB_DATABASE:-}}}" SOURCE_USER="${SOURCE_USER:-${CONFIG_DB_DEBUG_USER:-${CONFIG_DB_USER:-}}}" SOURCE_PASSWORD="${SOURCE_PASSWORD:-${CONFIG_DB_DEBUG_PASSWORD:-${CONFIG_DB_PASSWORD:-}}}" else SOURCE_HOST="${SOURCE_HOST:-${CONFIG_DB_HOST:-}}" SOURCE_DATABASE="${SOURCE_DATABASE:-${CONFIG_DB_DATABASE:-}}" SOURCE_USER="${SOURCE_USER:-${CONFIG_DB_USER:-}}" SOURCE_PASSWORD="${SOURCE_PASSWORD:-${CONFIG_DB_PASSWORD:-}}" fi if [[ -z "${SOURCE_HOST}" || -z "${SOURCE_DATABASE}" || -z "${SOURCE_USER}" || -z "${SOURCE_PASSWORD}" ]]; then echo "Missing source DB credentials for target '${SOURCE_TARGET}'. Set CONFIG_DB_TARGET and CONFIG_DB_* / CONFIG_DB_DEBUG_* in .env or env vars." exit 1 fi if [[ -z "${CLONE_DATABASE}" ]]; then CLONE_DATABASE="${SOURCE_DATABASE}_test_clone" fi if ! command -v docker >/dev/null 2>&1; then echo "Docker is required but was not found in PATH." exit 1 fi if [[ "${FORCE}" != "1" ]]; then echo "This will clone data from the '${SOURCE_TARGET}' database target into a local Docker MySQL container." read -r -p "Type CLONE to continue: " confirmation if [[ "${confirmation}" != "CLONE" ]]; then echo "Cancelled." exit 1 fi fi remove_container_if_exists() { local name="$1" if docker ps -aq --filter "name=^${name}$" | grep -q .; then docker rm -f "${name}" >/dev/null fi } if [[ "${SKIP_BUILD}" != "1" ]]; then if docker image inspect "${PHP_IMAGE}" >/dev/null 2>&1 && [[ "${FORCE_BUILD}" != "1" ]]; then echo "Using cached PHP test image (${PHP_IMAGE}). Set FORCE_BUILD=1 to rebuild." else echo "Building PHP test image (${PHP_IMAGE})..." docker build -f services/php/Dockerfile -t "${PHP_IMAGE}" . fi fi if ! docker network ls --format '{{.Name}}' | grep -qx "${NETWORK_NAME}"; then echo "Creating docker network ${NETWORK_NAME}..." docker network create "${NETWORK_NAME}" >/dev/null fi echo "Resetting test containers..." remove_container_if_exists "${DB_CONTAINER_NAME}" remove_container_if_exists "${REDIS_CONTAINER_NAME}" echo "Starting cloned MySQL container (${DB_CONTAINER_NAME})..." docker run -d \ --name "${DB_CONTAINER_NAME}" \ --network "${NETWORK_NAME}" \ -e "MYSQL_ROOT_PASSWORD=${CLONE_ROOT_PASSWORD}" \ -e "MYSQL_DATABASE=${CLONE_DATABASE}" \ mysql:8.4 >/dev/null for i in $(seq 1 60); do if docker exec -e "MYSQL_PWD=${CLONE_ROOT_PASSWORD}" "${DB_CONTAINER_NAME}" sh -lc "mysqladmin -u root ping --silent" >/dev/null 2>&1; then break fi if [[ "$i" -eq 60 ]]; then echo "Timed out waiting for ${DB_CONTAINER_NAME} to be ready." exit 1 fi sleep 2 done echo "Cloning '${SOURCE_TARGET}' database ${SOURCE_DATABASE} from ${SOURCE_HOST} into ${DB_CONTAINER_NAME}/${CLONE_DATABASE}..." DUMP_DIR="${ROOT_DIR}/.tmp-db-clone" DUMP_FILE="${DUMP_DIR}/dump.sql" mkdir -p "${DUMP_DIR}" rm -f "${DUMP_FILE}" docker run --rm \ -e "MYSQL_PWD=${SOURCE_PASSWORD}" \ -v "${DUMP_DIR}:/dump" \ mysql:8.4 \ sh -lc "exec mysqldump --compress --single-transaction --quick --set-gtid-purged=OFF -h '${SOURCE_HOST}' -P '${SOURCE_PORT}' -u '${SOURCE_USER}' '${SOURCE_DATABASE}' > /dump/dump.sql" if [[ ! -f "${DUMP_FILE}" ]]; then echo "Database dump failed." exit 1 fi docker run --rm \ --network "${NETWORK_NAME}" \ -e "MYSQL_PWD=${CLONE_ROOT_PASSWORD}" \ -v "${DUMP_DIR}:/dump" \ mysql:8.4 \ sh -lc "exec mysql -h '${DB_CONTAINER_NAME}' -u root '${CLONE_DATABASE}' < /dump/dump.sql" rm -f "${DUMP_FILE}" echo "Starting isolated Redis container (${REDIS_CONTAINER_NAME})..." docker run -d --name "${REDIS_CONTAINER_NAME}" --network "${NETWORK_NAME}" redis:7 >/dev/null if [[ "${SKIP_TESTS}" != "1" ]]; then echo "Running tests in a separate PHP container against cloned DB..." docker run --rm \ --network "${NETWORK_NAME}" \ --env-file "${ROOT_DIR}/.env" \ -e "CONFIG_DB_HOST=${DB_CONTAINER_NAME}" \ -e "CONFIG_DB_DATABASE=${CLONE_DATABASE}" \ -e "CONFIG_DB_USER=root" \ -e "CONFIG_DB_PASSWORD=${CLONE_ROOT_PASSWORD}" \ -e "REDIS_CONFIG_HOST=${REDIS_CONTAINER_NAME}" \ -e "AUTO_COMPOSER_INSTALL=false" \ -v "${ROOT_DIR}/services/nginx/app:/var/www/html" \ -v "${ROOT_DIR}/services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro" \ "${PHP_IMAGE}" \ sh -lc "cd /var/www/html && if [ ! -f vendor/autoload.php ]; then composer install --no-interaction --prefer-dist; fi && ${TEST_COMMAND}" fi if [[ "${KEEP_CONTAINERS}" != "1" ]]; then echo "Cleaning up test containers..." remove_container_if_exists "${REDIS_CONTAINER_NAME}" remove_container_if_exists "${DB_CONTAINER_NAME}" else echo "Keeping containers for inspection:" echo " DB: ${DB_CONTAINER_NAME}" echo " Redis: ${REDIS_CONTAINER_NAME}" fi echo "Done."