import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] def test_canonical_compose_keeps_runtime_data_outside_the_source_checkout(): compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") dockerignore = (ROOT / ".dockerignore").read_text(encoding="utf-8").splitlines() gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8").splitlines() assert "${APPOPS_DATA_PATH:-./data}:/data" in compose assert "APPOPS_SECRET_KEY_FILE: /data/secret-key" in compose assert "- .:/data" not in compose assert "data" in dockerignore assert "/data/" in gitignore def test_canonical_compose_uses_explicit_publish_host_and_persistent_voice_token(): compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") entrypoint = (ROOT / "docker" / "entrypoint.sh").read_text(encoding="utf-8") assert "${APPOPS_PUBLISH_HOST:-127.0.0.1}:1216:1216" in compose assert "APP_OPS_VOICE_TOKEN_FILE=/data/voice-token" in dockerfile assert 'generate_service_token "true" "${APP_OPS_VOICE_TOKEN:-}"' in entrypoint assert '"${APP_OPS_VOICE_TOKEN_FILE:-${DATA_DIR}/voice-token}"' in entrypoint def test_canonical_compose_keeps_reverse_proxy_network_after_redeploy(): compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") assert "- appops_proxy" in compose assert "external: true" in compose assert "name: ${APPOPS_PROXY_NETWORK:-itworx-mcp-hub-npm}" in compose def test_canonical_compose_forwards_trusted_operator_proxy_configuration(): compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") assert 'APPOPS_OPERATOR_PROXY_AUTH: "${APPOPS_OPERATOR_PROXY_AUTH:-false}"' in compose assert 'APPOPS_OPERATOR_PROXY_TRUSTED_NETWORKS: "${APPOPS_OPERATOR_PROXY_TRUSTED_NETWORKS:-}"' in compose assert 'APPOPS_AUTH_ADMIN_SUBJECTS: "${APPOPS_AUTH_ADMIN_SUBJECTS:-}"' in compose assert 'APPOPS_AUTHENTIK_CLIENT_SECRET_FILE: /data/authentik-oidc-secret' in compose assert 'APPOPS_OPERATOR_PROXY_USERS:' not in compose assert 'APPOPS_OPERATOR_PROXY_GROUPS:' not in compose def test_canonical_compose_mounts_only_the_file_based_gitea_observer_secret(): compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") assert "GITEA_URL: https://gitea.example.com" in compose assert "GITEA_ADMIN_TOKEN_FILE: /data/gitea-admin-token" in compose assert "GITEA_ADMIN_TOKEN:" not in compose assert ( "GITEA_RUNNER_TARGETS_JSON: '{}'" in compose ) def test_runtime_image_label_matches_the_app_version(): dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") version = (ROOT / "backend/app/core/version.py").read_text(encoding="utf-8") expected_provenance = { "APPOPS_BUILD_ID": "org.opencontainers.image.version", "APPOPS_SOURCE_REVISION": "org.opencontainers.image.revision", "APPOPS_SOURCE_REPOSITORY": "org.opencontainers.image.source", "APPOPS_SOURCE_TREE": "org.opencontainers.image.source-tree", "APPOPS_BUILD_DATE": "org.opencontainers.image.created", } for build_argument, image_label in expected_provenance.items(): assert f"ARG {build_argument}=" in dockerfile assert f'{image_label}="${{{build_argument}}}"' in dockerfile assert f'{build_argument}: "${{{build_argument}:-' in compose assert 'io.itworx.app.version="1.8.0"' in dockerfile assert 'io.itworx.app.user-auth="oidc-rbac"' in dockerfile assert 'APP_VERSION = "1.8.0"' in version def test_unraid_upgrade_can_restore_voice_token_without_logging_it(): script = (ROOT / "scripts" / "upgrade_single_1216.sh").read_text(encoding="utf-8") assert "for env_key in \\\n APP_OPS_VOICE_TOKEN \\" in script assert 'printf \'%s=%s\\n\' "$env_key" "$env_value" >> "$ENV_FILE"' in script assert 'log "${env_value}"' not in script def test_upgrade_preserves_file_based_voice_credential(tmp_path): bash = shutil.which("bash") git_bash = Path("C:/Program Files/Git/bin/bash.exe") if git_bash.exists(): bash = str(git_bash) if not bash: pytest.skip("Bash is unavailable") script = (ROOT / "scripts" / "upgrade_single_1216.sh").read_text(encoding="utf-8") voice = script.split('VOICE_TOKEN_VALUE="$(awk', 1)[1].split("# Optional, explicit deployment-time overrides.", 1)[0] voice = 'VOICE_TOKEN_VALUE="$(awk' + voice credential = "existing-service-credential-must-survive-upgrade" (tmp_path / "voice-token").write_text(credential) (tmp_path / "runtime.env").write_text("APP_OPS_VOICE_TOKEN_FILE=/data/voice-token\n") result = subprocess.run([bash, "-s"], input=f'''set -eu APPDATA='{tmp_path.as_posix()}' ENV_FILE="$APPDATA/runtime.env" log() {{ echo unexpected_rotation; }} {voice} test "$VOICE_TOKEN_VALUE" = '{credential}' ! grep -q '^APP_OPS_VOICE_TOKEN=' "$ENV_FILE" ''', text=True, capture_output=True, check=False) assert result.returncode == 0, result.stderr assert "unexpected_rotation" not in result.stdout