diff --git a/tests/compose.ci-erpnext.yml b/tests/compose.ci-erpnext.yml new file mode 100644 index 00000000..77ab7136 --- /dev/null +++ b/tests/compose.ci-erpnext.yml @@ -0,0 +1,23 @@ +version: "3.9" + +services: + backend: + image: localhost:5000/frappe/erpnext-backend:latest + + frontend: + image: localhost:5000/frappe/erpnext-frontend:latest + + websocket: + image: localhost:5000/frappe/socketio:latest + + queue-short: + image: localhost:5000/frappe/erpnext-backend:latest + + queue-default: + image: localhost:5000/frappe/erpnext-backend:latest + + queue-long: + image: localhost:5000/frappe/erpnext-backend:latest + + scheduler: + image: localhost:5000/frappe/erpnext-backend:latest diff --git a/tests/compose.ci.yml b/tests/compose.ci.yml new file mode 100644 index 00000000..f1138162 --- /dev/null +++ b/tests/compose.ci.yml @@ -0,0 +1,23 @@ +version: "3.9" + +services: + backend: + image: localhost:5000/frappe/backend:latest + + frontend: + image: localhost:5000/frappe/frontend:latest + + websocket: + image: localhost:5000/frappe/socketio:latest + + queue-short: + image: localhost:5000/frappe/backend:latest + + queue-default: + image: localhost:5000/frappe/backend:latest + + queue-long: + image: localhost:5000/frappe/backend:latest + + scheduler: + image: localhost:5000/frappe/backend:latest diff --git a/tests/functions.sh b/tests/functions.sh deleted file mode 100644 index e077dbd9..00000000 --- a/tests/functions.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -print_group() { - echo ::endgroup:: - echo "::group::$*" -} - -ping_site() { - print_group "Ping site $SITE_NAME" - - echo Ping version - ping_res=$(curl -sS "http://$SITE_NAME/api/method/version") - echo "$ping_res" - if [[ -z $(echo "$ping_res" | grep "message" || echo "") ]]; then - echo "Ping failed" - exit 1 - fi - - echo Check index - index_res=$(curl -sS "http://$SITE_NAME") - if [[ -n $(echo "$index_res" | grep "Internal Server Error" || echo "") ]]; then - echo "Index check failed" - echo "$index_res" - exit 1 - fi -} diff --git a/tests/integration-test.sh b/tests/integration-test.sh deleted file mode 100755 index a76a5479..00000000 --- a/tests/integration-test.sh +++ /dev/null @@ -1,283 +0,0 @@ -#!/bin/bash - -set -e - -source tests/functions.sh - -project_name=frappe_bench_00 - -docker_compose_with_args() { - # shellcheck disable=SC2068 - docker-compose \ - -p $project_name \ - -f installation/docker-compose-common.yml \ - -f installation/docker-compose-frappe.yml \ - -f installation/frappe-publish.yml \ - $@ -} - -check_migration_complete() { - print_group Check migration - - container_id=$(docker_compose_with_args ps -q frappe-python) - cmd="docker logs ${container_id} 2>&1 | grep 'Starting gunicorn' || echo ''" - worker_log=$(eval "$cmd") - INCREMENT=0 - - while [[ ${worker_log} != *"Starting gunicorn"* && ${INCREMENT} -lt 120 ]]; do - sleep 3 - ((INCREMENT = INCREMENT + 1)) - echo "Wait for migration to complete..." - worker_log=$(eval "$cmd") - if [[ ${worker_log} != *"Starting gunicorn"* && ${INCREMENT} -eq 120 ]]; then - echo Migration timeout - docker logs "${container_id}" - exit 1 - fi - done - - echo Migration Log - docker logs "${container_id}" -} - -check_health() { - print_group Loop health check - - docker run --name frappe_doctor \ - -v "${project_name}_sites-vol:/home/frappe/frappe-bench/sites" \ - --network "${project_name}_default" \ - frappe/frappe-worker:edge doctor || true - - cmd='docker logs frappe_doctor | grep "Health check successful" || echo ""' - doctor_log=$(eval "$cmd") - INCREMENT=0 - - while [[ -z "${doctor_log}" && ${INCREMENT} -lt 60 ]]; do - sleep 1 - ((INCREMENT = INCREMENT + 1)) - container=$(docker start frappe_doctor) - echo "Restarting ${container}..." - doctor_log=$(eval "$cmd") - - if [[ ${INCREMENT} -eq 60 ]]; then - docker logs "${container}" - exit 1 - fi - done -} - -# Initial group -echo ::group::Setup .env -cp env-example .env -sed -i -e "s/edge/v13/g" .env -cat .env -# shellcheck disable=SC2046 -export $(cat .env) - -print_group Start services -echo Start main services -docker_compose_with_args up -d --quiet-pull - -echo Start postgres -docker pull postgres:11.8 -q -docker run \ - --name postgresql \ - -d \ - -e POSTGRES_PASSWORD=admin \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - postgres:11.8 - -check_health - -print_group "Create new site " -SITE_NAME=test.localhost -docker run \ - --rm \ - -e SITE_NAME=$SITE_NAME \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:v13 new - -ping_site - -print_group "Update .env (v13 -> edge)" -sed -i -e "s/v13/edge/g" .env -cat .env -# shellcheck disable=SC2046 -export $(cat .env) - -print_group Restart containers -docker_compose_with_args stop -docker_compose_with_args up -d - -check_migration_complete -sleep 5 -ping_site - -PG_SITE_NAME=pgsql.localhost -print_group "Create new site (Postgres)" -docker run \ - --rm \ - -e SITE_NAME=$PG_SITE_NAME \ - -e POSTGRES_HOST=postgresql \ - -e DB_ROOT_USER=postgres \ - -e POSTGRES_PASSWORD=admin \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge new - -check_migration_complete -SITE_NAME=$PG_SITE_NAME ping_site - -print_group Backup site -docker run \ - --rm \ - -e WITH_FILES=1 \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge backup - -MINIO_ACCESS_KEY="AKIAIOSFODNN7EXAMPLE" -MINIO_SECRET_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" - -print_group Prepare S3 server -echo Start S3 server -docker run \ - --name minio \ - -d \ - -e "MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY" \ - -e "MINIO_SECRET_KEY=$MINIO_SECRET_KEY" \ - --network ${project_name}_default \ - minio/minio server /data - -echo Create bucket -docker run \ - --rm \ - --network ${project_name}_default \ - vltgroup/s3cmd:latest \ - s3cmd \ - --access_key=$MINIO_ACCESS_KEY \ - --secret_key=$MINIO_SECRET_KEY \ - --region=us-east-1 \ - --no-ssl \ - --host=minio:9000 \ - --host-bucket=minio:9000 \ - mb s3://frappe - -print_group Push backup -docker run \ - --rm \ - -e BUCKET_NAME=frappe \ - -e REGION=us-east-1 \ - -e BUCKET_DIR=local \ - -e ACCESS_KEY_ID=$MINIO_ACCESS_KEY \ - -e SECRET_ACCESS_KEY=$MINIO_SECRET_KEY \ - -e ENDPOINT_URL=http://minio:9000 \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge push-backup - -print_group Prune and restart services -docker_compose_with_args stop -docker container prune -f && docker volume prune -f -docker_compose_with_args up -d - -check_health - -print_group Restore backup from S3 -docker run \ - --rm \ - -e MYSQL_ROOT_PASSWORD=admin \ - -e BUCKET_NAME=frappe \ - -e BUCKET_DIR=local \ - -e ACCESS_KEY_ID=$MINIO_ACCESS_KEY \ - -e SECRET_ACCESS_KEY=$MINIO_SECRET_KEY \ - -e ENDPOINT_URL=http://minio:9000 \ - -e REGION=us-east-1 \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge restore-backup - -check_health -ping_site -SITE_NAME=$PG_SITE_NAME ping_site - -EDGE_SITE_NAME=edge.localhost -print_group "Create new site (edge)" -docker run \ - --rm \ - -e SITE_NAME=$EDGE_SITE_NAME \ - -e INSTALL_APPS=frappe \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge new - -check_health -SITE_NAME=$EDGE_SITE_NAME ping_site - -print_group Migrate edge site -docker run \ - --rm \ - -e MAINTENANCE_MODE=1 \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - -v ${project_name}_assets-vol:/home/frappe/frappe-bench/sites/assets \ - --network ${project_name}_default \ - frappe/frappe-worker:edge migrate - -check_migration_complete - -print_group "Restore backup S3 (overwrite)" -docker run \ - --rm \ - -e MYSQL_ROOT_PASSWORD=admin \ - -e BUCKET_NAME=frappe \ - -e BUCKET_DIR=local \ - -e ACCESS_KEY_ID=$MINIO_ACCESS_KEY \ - -e SECRET_ACCESS_KEY=$MINIO_SECRET_KEY \ - -e ENDPOINT_URL=http://minio:9000 \ - -e REGION=us-east-1 \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge restore-backup - -check_migration_complete -ping_site - -print_group "Check console for $SITE_NAME" -docker run \ - --rm \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge console $SITE_NAME - -print_group "Check console for $PG_SITE_NAME" -docker run \ - --rm \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge console $PG_SITE_NAME - -print_group "Check drop site for $SITE_NAME (MariaDB)" -docker run \ - --rm \ - -e SITE_NAME=$SITE_NAME \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge drop - -print_group "Check drop site for $PG_SITE_NAME (Postgres)" -docker run \ - --rm \ - -e SITE_NAME=$PG_SITE_NAME \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:edge drop - -print_group Check bench --help -docker run \ - --rm \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - --user frappe \ - frappe/frappe-worker:edge bench --help diff --git a/tests/main.py b/tests/main.py new file mode 100644 index 00000000..61e9fe48 --- /dev/null +++ b/tests/main.py @@ -0,0 +1,330 @@ +import os +import ssl +import subprocess +from enum import Enum +from functools import wraps +from time import sleep +from typing import Any, Callable, Optional +from urllib.error import HTTPError +from urllib.request import Request, urlopen + +CI = os.getenv("CI") +SITE_NAME = "tests" +BACKEND_SERVICES = ( + "backend", + "queue-short", + "queue-default", + "queue-long", + "scheduler", +) + + +def patch_print(): + # Patch `print()` builtin to have nice logs when running GitHub Actions + if not CI: + return + global print + _old_print = print + + def print( + *values: Any, + sep: Optional[str] = None, + end: Optional[str] = None, + file: Any = None, + flush: bool = False, + ): + return _old_print(*values, sep=sep, end=end, file=file, flush=True) + + +class Color(Enum): + GREY = 30 + RED = 31 + GREEN = 32 + YELLOW = 33 + BLUE = 34 + MAGENTA = 35 + CYAN = 36 + WHITE = 37 + + +def colored(text: str, color: Color): + return f"\033[{color.value}m{text}\033[0m" + + +def log(text: str): + def decorator(f: Callable[..., Any]): + @wraps(f) + def wrapper(*args: Any, **kwargs: Any): + if CI: + print(f"::group::{text}") + else: + print(colored(text, Color.YELLOW)) + ret = f(*args, **kwargs) + if CI: + print("::endgroup::") + return ret + + return wrapper + + return decorator + + +def run(*cmd: str): + print(colored(f"> {' '.join(cmd)}", Color.GREEN)) + return subprocess.check_call(cmd) + + +def docker_compose(*cmd: str): + args = [ + "docker", + "compose", + "-p", + "test", + "--env-file", + "example.env", + "-f", + "compose.yml", + ] + if CI: + args.extend(("-f", "tests/compose.ci.yml")) + return run(*args, *cmd) + + +@log("Create containers") +def create_containers(): + docker_compose("up", "-d") + + +@log("Check if backend services have connections") +def ping_links_in_backends(): + for service in BACKEND_SERVICES: + for _ in range(10): + try: + docker_compose("exec", service, "healthcheck.sh") + break + except subprocess.CalledProcessError: + sleep(1) + else: + raise Exception(f"Connections healthcheck failed for service {service}") + + +@log("Create test site") +def create_site(): + docker_compose( + "exec", + "backend", + "bench", + "new-site", + SITE_NAME, + "--mariadb-root-password", + "123", + "--admin-password", + "admin", + ) + docker_compose("restart", "backend") + + +# This is needed to check https override +_ssl_ctx = ssl.create_default_context() +_ssl_ctx.check_hostname = False +_ssl_ctx.verify_mode = ssl.CERT_NONE + + +def ping_and_check_content(url: str, callback: Callable[[str], Optional[str]]): + request = Request(url, headers={"Host": SITE_NAME}) + print(f"Checking {url}") + for _ in range(100): + try: + response = urlopen(request, context=_ssl_ctx) + except HTTPError as exc: + if exc.code not in (404, 502): + raise + else: + text: str = response.read().decode() + ret = callback(text) + if ret: + print(ret) + return + + sleep(0.1) + raise AssertionError(f"Couldn't ping {url}") + + +def index_callback(text: str): + if "404 page not found" not in text: + return text[:200] + + +@log("Check /") +def check_index(): + ping_and_check_content(url="http://127.0.0.1", callback=index_callback) + + +@log("Check /api/method/version") +def check_api(): + ping_and_check_content( + url="http://127.0.0.1/api/method/version", + callback=lambda text: text if '"message"' in text else None, + ) + + +@log("Check if Frappe can connect to services in backends") +def ping_frappe_connections_in_backends(): + for service in BACKEND_SERVICES: + docker_compose("cp", f"tests/ping_frappe_connections.py", f"{service}:/tmp/") + docker_compose( + "exec", + service, + "/home/frappe/frappe-bench/env/bin/python", + f"/tmp/ping_frappe_connections.py", + ) + + +@log("Check /assets") +def check_assets(): + ping_and_check_content( + url="http://127.0.0.1/assets/js/frappe-web.min.js", + callback=lambda text: text[:200] if text is not None else None, + ) + + +@log("Check /files") +def check_files(): + file_name = "testfile.txt" + docker_compose( + "cp", + f"tests/{file_name}", + f"backend:/home/frappe/frappe-bench/sites/{SITE_NAME}/public/files/", + ) + ping_and_check_content( + url=f"http://127.0.0.1/files/{file_name}", + callback=lambda text: text if text == "lalala\n" else None, + ) + + +@log("Recreate with https override") +def recreate_with_https_override(): + docker_compose("-f", "overrides/compose.https.yml", "up", "-d") + + +@log("Check / (https)") +def check_index_https(): + ping_and_check_content(url="https://127.0.0.1", callback=index_callback) + + +@log("Stop containers") +def stop_containers(): + docker_compose("down", "-v", "--remove-orphans") + + +@log("Recreate with ERPNext override") +def create_containers_with_erpnext_override(): + args = ["-f", "overrides/compose.erpnext.yml"] + if CI: + args.extend(("-f", "tests/compose.ci-erpnext.yml")) + docker_compose(*args, "up", "-d") + + +@log("Create ERPNext site") +def create_erpnext_site(): + docker_compose( + "exec", + "backend", + "bench", + "new-site", + SITE_NAME, + "--mariadb-root-password", + "123", + "--admin-password", + "admin", + "--install-app", + "erpnext", + ) + docker_compose("restart", "backend") + + +@log("Check /api/method/erpnext.templates.pages.product_search.get_product_list") +def check_erpnext_api(): + ping_and_check_content( + url="http://127.0.0.1/api/method/erpnext.templates.pages.product_search.get_product_list", + callback=lambda text: text if '"message"' in text else None, + ) + + +@log("Check /assets/erpnext/js/setup_wizard.js") +def check_erpnext_assets(): + ping_and_check_content( + url="http://127.0.0.1/assets/erpnext/js/setup_wizard.js", + callback=lambda text: text[:200] if text is not None else None, + ) + + +@log("Create containers with Postgres override") +def create_containers_with_postgres_override(): + docker_compose("-f", "overrides/compose.postgres.yml", "up", "-d") + + +@log("Create Postgres site") +def create_postgres_site(): + docker_compose( + "exec", "backend", "bench", "set-config", "-g", "root_login", "postgres" + ) + docker_compose( + "exec", "backend", "bench", "set-config", "-g", "root_password", "123" + ) + docker_compose( + "exec", + "backend", + "bench", + "new-site", + SITE_NAME, + "--db-type", + "postgres", + "--admin-password", + "admin", + ) + docker_compose("restart", "backend") + + +@log("Show docker compose logs") +def show_docker_compose_logs(): + docker_compose("logs") + + +def main() -> int: + try: + patch_print() + + create_containers() + ping_links_in_backends() + create_site() + check_index() + check_api() + ping_frappe_connections_in_backends() + check_assets() + check_files() + + recreate_with_https_override() + check_index_https() + stop_containers() + + create_containers_with_erpnext_override() + create_erpnext_site() + check_erpnext_api() + check_erpnext_assets() + stop_containers() + + create_containers_with_postgres_override() + create_postgres_site() + ping_links_in_backends() + + finally: + show_docker_compose_logs() + stop_containers() + + print(colored("Tests successfully passed!", Color.YELLOW)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/ping_frappe_connections.py b/tests/ping_frappe_connections.py new file mode 100644 index 00000000..da08301f --- /dev/null +++ b/tests/ping_frappe_connections.py @@ -0,0 +1,26 @@ +import frappe + + +def check_db(): + doc = frappe.get_single("System Settings") + assert any(v is None for v in doc.as_dict().values()), "Database test didn't pass" + print("Database works!") + + +def check_cache(): + key_and_name = "mytestkey", "mytestname" + frappe.cache().hset(*key_and_name, "mytestvalue") + assert frappe.cache().hget(*key_and_name) == "mytestvalue", "Cache test didn't pass" + frappe.cache().hdel(*key_and_name) + print("Cache works!") + + +def main() -> int: + frappe.connect(site="tests") + check_db() + check_cache() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..9957231e --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +frappe @ git+git://github.com/frappe/frappe.git +boto3-stubs[s3]==1.20.23 diff --git a/tests/test-erpnext.sh b/tests/test-erpnext.sh deleted file mode 100755 index c83151a9..00000000 --- a/tests/test-erpnext.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -set -e - -source tests/functions.sh - -project_name="test_erpnext" -SITE_NAME="test_erpnext.localhost" - -echo ::group::Setup env -cp env-example .env -sed -i -e "s/edge/test/g" .env -# shellcheck disable=SC2046 -export $(cat .env) -cat .env - -print_group Start services -docker-compose \ - -p $project_name \ - -f installation/docker-compose-common.yml \ - -f installation/docker-compose-erpnext.yml \ - -f installation/erpnext-publish.yml \ - up -d - -print_group Fix permissions -docker run \ - --rm \ - --user root \ - -v ${project_name}_sites-vol:/sites \ - -v ${project_name}_assets-vol:/assets \ - -v ${project_name}_logs-vol:/logs \ - frappe/erpnext-worker:test chown -R 1000:1000 /logs /sites /assets - -print_group Create site -docker run \ - --rm \ - -e "SITE_NAME=$SITE_NAME" \ - -e "INSTALL_APPS=erpnext" \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/erpnext-worker:test new - -ping_site -rm .env diff --git a/tests/test-frappe.sh b/tests/test-frappe.sh deleted file mode 100755 index 12131839..00000000 --- a/tests/test-frappe.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -set -e - -source tests/functions.sh - -project_name="test_frappe" -SITE_NAME="test_frappe.localhost" - -docker_compose_with_args() { - # shellcheck disable=SC2068 - docker-compose \ - -p $project_name \ - -f installation/docker-compose-common.yml \ - -f installation/docker-compose-frappe.yml \ - -f installation/frappe-publish.yml \ - $@ -} - -echo ::group::Setup env -cp env-example .env -sed -i -e "s/edge/test/g" .env -# shellcheck disable=SC2046 -export $(cat .env) -cat .env - -print_group Start services -docker_compose_with_args up -d - -print_group Create site -docker run \ - --rm \ - -e "SITE_NAME=$SITE_NAME" \ - -v ${project_name}_sites-vol:/home/frappe/frappe-bench/sites \ - --network ${project_name}_default \ - frappe/frappe-worker:test new - -ping_site - -print_group Stop and remove containers -docker_compose_with_args down - -rm .env diff --git a/tests/testfile.txt b/tests/testfile.txt new file mode 100644 index 00000000..f22355f5 --- /dev/null +++ b/tests/testfile.txt @@ -0,0 +1 @@ +lalala