diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 7e0ed2cb9cc..6ce7e242fab 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -9,8 +9,8 @@ jobs: name: coding-standard runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: php-version: "8.4" extensions: curl, xml, mbstring, zip @@ -27,8 +27,8 @@ jobs: name: check-gherkin-standard runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: "24" - name: Lint feature files @@ -40,18 +40,18 @@ jobs: name: check-suites-in-expected-failures runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v4 - name: Check suites run: bash tests/acceptance/check-deleted-suites-in-expected-failure.sh - + build-and-test: - needs: [coding-standard, check-gherkin-standard, check-suites-in-expected-failures] name: build-and-test + needs: [coding-standard, check-gherkin-standard, check-suites-in-expected-failures] runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v4 - - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + - uses: actions/setup-go@v5 with: go-version-file: go.mod cache: true @@ -62,8 +62,7 @@ jobs: - name: Unit tests run: make test - - acceptance-tests: + local-api-tests: name: ${{ matrix.suite }} needs: [build-and-test] runs-on: ubuntu-latest @@ -71,10 +70,17 @@ jobs: fail-fast: false matrix: suite: + # contract & locks + - apiContract + - apiLocks + # settings & notifications (needs email) + - apiSettings + - apiNotification + - apiCors # graph + - apiGraphUser - apiGraph - apiGraphGroup - - apiGraphUser # spaces & dav - apiSpaces - apiSpacesShares @@ -86,18 +92,26 @@ jobs: - apiActivities # search - apiSearch1 - # contract & locks - - apiLocks + - apiSearch2 + - apiSearchContent # needs Tika # sharing - - apiSharingNgItemInvitation - - apiSharingNgPermissions - apiSharingNgShares + - apiReshare + - apiSharingNgPermissions - apiSharingNgAdditionalShareRole - apiSharingNgDriveInvitation - - apiSharingNgItemLinkShare + - apiSharingNgItemInvitation - apiSharingNgDriveLinkShare + - apiSharingNgItemLinkShare - apiSharingNgLinkShareManagement - + # auth + - apiAuthApp + # antivirus (needs ClamAV) + - apiAntivirus + # federation (needs email + federation ocis) + - apiOcm + # collaboration (needs WOPI) + - apiCollaboration steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -105,6 +119,28 @@ jobs: with: go-version-file: go.mod cache: true + + - name: Install libcurl 8.12.0 from source + run: | + sudo apt-get update -qq + sudo apt-get install -y libssl-dev libnghttp2-dev libpsl-dev libldap-dev libssh-dev zlib1g-dev + cd /tmp + curl -sLO https://curl.se/download/curl-8.12.0.tar.gz + tar xzf curl-8.12.0.tar.gz + cd curl-8.12.0 + ./configure --with-ssl --with-zlib --with-nghttp2 --prefix=/usr/local --silent + make -j$(nproc) --silent + sudo make install --silent + sudo ldconfig + curl --version | head -1 + php -r ' + $v = curl_version()["version"]; + echo "PHP curl: $v\n"; + if (version_compare($v, "8.12.0", "<")) { + fwrite(STDERR, "FATAL: PHP sees libcurl $v, need >= 8.12.0\n"); + exit(1); + } + ' - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 with: @@ -112,11 +148,157 @@ jobs: extensions: curl, xml, mbstring, zip tools: composer + - name: Run ${{ matrix.suite }} + run: BEHAT_SUITES=${{ matrix.suite }} python3 tests/acceptance/run-github.py + + cli-tests: + name: ${{ matrix.suite }} + needs: [build-and-test] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + suite: + - cliCommands # needs ClamAV + email + - apiServiceAvailability + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.4" + extensions: curl, xml, mbstring, zip + tools: composer + - name: Run ${{ matrix.suite }} run: BEHAT_SUITES=${{ matrix.suite }} bash tests/acceptance/run-github.sh + - name: Upload test logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-logs-${{ matrix.suite }} + path: tests/acceptance/output/ + + core-api-tests: + name: ${{ matrix.suite }} + needs: [build-and-test] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + suite: + - "coreApiAuth,coreApiCapabilities,coreApiFavorites,coreApiMain,coreApiVersions" + - "coreApiShareManagementBasicToShares,coreApiShareManagementToShares" + - "coreApiSharees,coreApiSharePublicLink2" + - "coreApiShareOperationsToShares1,coreApiShareOperationsToShares2,coreApiSharePublicLink1,coreApiShareCreateSpecialToShares1,coreApiShareCreateSpecialToShares2,coreApiShareUpdateToShares" + - "coreApiTrashbin,coreApiTrashbinRestore,coreApiWebdavEtagPropagation1,coreApiWebdavEtagPropagation2" + - "coreApiWebdavDelete,coreApiWebdavOperations,coreApiWebdavMove2" + - "coreApiWebdavProperties" + - "coreApiWebdavMove1,coreApiWebdavPreviews,coreApiWebdavUpload,coreApiWebdavUploadTUS" + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.4" + extensions: curl, xml, mbstring, zip + tools: composer + + - name: Run ${{ matrix.suite }} + run: > + BEHAT_SUITES="${{ matrix.suite }}" + ACCEPTANCE_TEST_TYPE=core-api + EXPECTED_FAILURES_FILE=tests/acceptance/expected-failures-API-on-OCIS-storage.md + bash tests/acceptance/run-github.sh + + - name: Upload test logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-logs-${{ matrix.suite }} + path: tests/acceptance/output/ + + e2e-tests: + name: e2e-${{ matrix.suite }} + needs: [build-and-test] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - suite: part-1 + args: "--total-parts 4 --xsuites search,app-provider,ocm,keycloak --run-part 1" + - suite: part-2 + args: "--total-parts 4 --xsuites search,app-provider,ocm,keycloak --run-part 2" + - suite: part-3 + args: "--total-parts 4 --xsuites search,app-provider,ocm,keycloak --run-part 3" + - suite: part-4 + args: "--total-parts 4 --xsuites search,app-provider,ocm,keycloak --run-part 4" + - suite: search + args: "--suites search" # needs Tika + - suite: keycloak + args: "--suites journeys,keycloak" # needs postgres + keycloak + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Run e2e-${{ matrix.suite }} + run: E2E_ARGS="${{ matrix.args }}" bash tests/acceptance/run-e2e.sh + + - name: Upload tracing results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-tracing-${{ matrix.suite }} + path: webTestRunner/reports/e2e/playwright/tracing/ + + litmus: + name: litmus + needs: [build-and-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Run litmus + run: bash tests/acceptance/run-litmus.sh + + cs3-api-tests: + name: cs3ApiTests + needs: [build-and-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Run cs3ApiTests + run: bash tests/acceptance/run-cs3api.sh + all-acceptance-tests: - needs: [acceptance-tests] + needs: [build-and-test, local-api-tests, cli-tests, core-api-tests, e2e-tests, litmus, cs3-api-tests] runs-on: ubuntu-latest if: always() steps: diff --git a/tests/acceptance/run-cs3api.sh b/tests/acceptance/run-cs3api.sh new file mode 100644 index 00000000000..89a74c8b201 --- /dev/null +++ b/tests/acceptance/run-cs3api.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +OCIS_BIN="$REPO_ROOT/ocis/bin/ocis" +WRAPPER_BIN="$REPO_ROOT/tests/ociswrapper/bin/ociswrapper" +OCIS_URL="https://localhost:9200" +OCIS_CONFIG_DIR="$HOME/.ocis/config" + +# build +make -C "$REPO_ROOT/ocis" build +GOWORK=off make -C "$REPO_ROOT/tests/ociswrapper" build + +# init + start ocis with gRPC gateway exposed for cs3api-validator +"$OCIS_BIN" init --insecure true +cp "$REPO_ROOT/tests/config/drone/app-registry.yaml" "$OCIS_CONFIG_DIR/app-registry.yaml" + +OCIS_URL=$OCIS_URL \ +OCIS_CONFIG_DIR=$OCIS_CONFIG_DIR \ +STORAGE_USERS_DRIVER=ocis \ +PROXY_ENABLE_BASIC_AUTH=true \ +OCIS_EXCLUDE_RUN_SERVICES=idp \ +OCIS_LOG_LEVEL=error \ +IDM_CREATE_DEMO_USERS=true \ +IDM_ADMIN_PASSWORD=admin \ +OCIS_ASYNC_UPLOADS=true \ +OCIS_EVENTS_ENABLE_TLS=false \ +NATS_NATS_HOST=0.0.0.0 \ +NATS_NATS_PORT=9233 \ +OCIS_JWT_SECRET=some-ocis-jwt-secret \ +GATEWAY_GRPC_ADDR=0.0.0.0:9142 \ +OCIS_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD=false \ +WEB_UI_CONFIG_FILE="$REPO_ROOT/tests/config/drone/ocis-config.json" \ + "$WRAPPER_BIN" serve \ + --bin "$OCIS_BIN" \ + --url "$OCIS_URL" \ + --admin-username admin \ + --admin-password admin & +WRAPPER_PID=$! +trap "kill $WRAPPER_PID 2>/dev/null || true" EXIT + +echo "Waiting for ocis..." +timeout 300 bash -c \ + "while [ \$(curl -sk -uadmin:admin $OCIS_URL/graph/v1.0/users/admin \ + -w %{http_code} -o /dev/null) != 200 ]; do sleep 1; done" +echo "ocis ready." + +docker run --rm --network host \ + owncloud/cs3api-validator:0.2.1 \ + /usr/bin/cs3api-validator /var/lib/cs3api-validator --endpoint=localhost:9142 diff --git a/tests/acceptance/run-e2e.sh b/tests/acceptance/run-e2e.sh new file mode 100755 index 00000000000..3ff8a9cb2dc --- /dev/null +++ b/tests/acceptance/run-e2e.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +OCIS_BIN="$REPO_ROOT/ocis/bin/ocis" +WRAPPER_BIN="$REPO_ROOT/tests/ociswrapper/bin/ociswrapper" +OCIS_URL="https://localhost:9200" +OCIS_CONFIG_DIR="$HOME/.ocis/config" +WEB_DIR="$REPO_ROOT/webTestRunner" + +: "${E2E_ARGS:?E2E_ARGS is required, e.g. E2E_ARGS='--run-part 1' bash run-e2e.sh}" + +# build ocis + ociswrapper +make -C "$REPO_ROOT/ocis" build +GOWORK=off make -C "$REPO_ROOT/tests/ociswrapper" build + +# clone owncloud/web (test runner lives there) +if [ ! -d "$WEB_DIR" ]; then + git clone --depth 1 https://github.com/owncloud/web.git "$WEB_DIR" +fi +cd "$WEB_DIR" +npm install -g pnpm +pnpm install + +# init + start ocis +"$OCIS_BIN" init --insecure true +cp "$REPO_ROOT/tests/config/drone/app-registry.yaml" "$OCIS_CONFIG_DIR/app-registry.yaml" + +OCIS_URL=$OCIS_URL \ +OCIS_CONFIG_DIR=$OCIS_CONFIG_DIR \ +STORAGE_USERS_DRIVER=ocis \ +PROXY_ENABLE_BASIC_AUTH=true \ +OCIS_EXCLUDE_RUN_SERVICES=idp \ +OCIS_LOG_LEVEL=error \ +IDM_CREATE_DEMO_USERS=true \ +IDM_ADMIN_PASSWORD=admin \ +OCIS_ASYNC_UPLOADS=true \ +OCIS_EVENTS_ENABLE_TLS=false \ +NATS_NATS_HOST=0.0.0.0 \ +NATS_NATS_PORT=9233 \ +OCIS_JWT_SECRET=some-ocis-jwt-secret \ +WEB_UI_CONFIG_FILE="$REPO_ROOT/tests/config/drone/ocis-config.json" \ + "$WRAPPER_BIN" serve \ + --bin "$OCIS_BIN" \ + --url "$OCIS_URL" \ + --admin-username admin \ + --admin-password admin & +WRAPPER_PID=$! +trap "kill $WRAPPER_PID 2>/dev/null || true" EXIT + +echo "Waiting for ocis..." +timeout 300 bash -c \ + "while [ \$(curl -sk -uadmin:admin $OCIS_URL/graph/v1.0/users/admin \ + -w %{http_code} -o /dev/null) != 200 ]; do sleep 1; done" +echo "ocis ready." + +# run playwright e2e tests +cd "$WEB_DIR/tests/e2e" +echo "Running e2e: $E2E_ARGS" +BASE_URL_OCIS=$OCIS_URL \ +HEADLESS=true \ +RETRY=1 \ +SKIP_A11Y_TESTS=true \ +REPORT_TRACING=true \ + bash run-e2e.sh $E2E_ARGS diff --git a/tests/acceptance/run-github.py b/tests/acceptance/run-github.py new file mode 100755 index 00000000000..9d64f185ed0 --- /dev/null +++ b/tests/acceptance/run-github.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +""" +Run ocis acceptance tests locally and in GitHub Actions CI. + +Config sourced from .drone.star localApiTests — single source of truth. +Usage: BEHAT_SUITES=apiGraph python3 tests/acceptance/run-github.py +""" + +import os +import sys +import subprocess +import signal +import time +import tempfile +import shutil +from pathlib import Path + +# --------------------------------------------------------------------------- +# Config sourced from .drone.star +# NOTE: EMAIL_SMTP_HOST is "email" (container name) in drone, "localhost" here +# --------------------------------------------------------------------------- + +EMAIL_SMTP_HOST = "localhost" +EMAIL_SMTP_PORT = "1025" +EMAIL_PORT = "8025" +EMAIL_SMTP_SENDER = "ownCloud " + +LOCAL_API_TESTS = { + "contractAndLock": { + "suites": ["apiContract", "apiLocks"], + }, + "settingsAndNotification": { + "suites": ["apiSettings", "apiNotification", "apiCors"], + "emailNeeded": True, + "extraEnvironment": { + "EMAIL_HOST": EMAIL_SMTP_HOST, + "EMAIL_PORT": EMAIL_PORT, + }, + "extraServerEnvironment": { + "OCIS_ADD_RUN_SERVICES": "notifications", + "NOTIFICATIONS_SMTP_HOST": EMAIL_SMTP_HOST, + "NOTIFICATIONS_SMTP_PORT": EMAIL_SMTP_PORT, + "NOTIFICATIONS_SMTP_INSECURE": "true", + "NOTIFICATIONS_SMTP_SENDER": EMAIL_SMTP_SENDER, + "NOTIFICATIONS_DEBUG_ADDR": "0.0.0.0:9174", + }, + }, + "graphUser": { + "suites": ["apiGraphUser"], + }, + "spaces": { + "suites": ["apiSpaces"], + }, + "spacesShares": { + "suites": ["apiSpacesShares"], + }, + "davOperations": { + "suites": [ + "apiSpacesDavOperation", "apiDownloads", "apiAsyncUpload", + "apiDepthInfinity", "apiArchiver", "apiActivities", + ], + }, + "groupAndSearch1": { + "suites": ["apiSearch1", "apiGraph", "apiGraphGroup"], + }, + "search2": { + "suites": ["apiSearch2", "apiSearchContent"], + "tikaNeeded": True, + "extraServerEnvironment": { + "FRONTEND_FULL_TEXT_SEARCH_ENABLED": "true", + "SEARCH_EXTRACTOR_TYPE": "tika", + "SEARCH_EXTRACTOR_TIKA_TIKA_URL": "http://localhost:9998", + "SEARCH_EXTRACTOR_CS3SOURCE_INSECURE": "true", + }, + }, + "sharingNg1": { + "suites": ["apiSharingNgShares", "apiReshare", "apiSharingNgPermissions"], + }, + "sharingNgAdditionalShareRole": { + "suites": ["apiSharingNgAdditionalShareRole"], + }, + "sharingNgShareInvitation": { + "suites": ["apiSharingNgDriveInvitation", "apiSharingNgItemInvitation"], + }, + "sharingNgLinkShare": { + "suites": [ + "apiSharingNgDriveLinkShare", "apiSharingNgItemLinkShare", + "apiSharingNgLinkShareManagement", + ], + }, + "antivirus": { + "suites": ["apiAntivirus"], + "antivirusNeeded": True, + "extraServerEnvironment": { + "ANTIVIRUS_SCANNER_TYPE": "clamav", + "ANTIVIRUS_CLAMAV_SOCKET": "tcp://clamav:3310", + "POSTPROCESSING_STEPS": "virusscan", + "OCIS_ADD_RUN_SERVICES": "antivirus", + "ANTIVIRUS_DEBUG_ADDR": "0.0.0.0:9277", + }, + }, + "ocm": { + "suites": ["apiOcm"], + "emailNeeded": True, + "federationServer": True, + "extraEnvironment": { + "EMAIL_HOST": EMAIL_SMTP_HOST, + "EMAIL_PORT": EMAIL_PORT, + }, + "extraServerEnvironment": { + "OCIS_ADD_RUN_SERVICES": "ocm,notifications", + "OCIS_ENABLE_OCM": "true", + "OCM_OCM_INVITE_MANAGER_INSECURE": "true", + "OCM_OCM_SHARE_PROVIDER_INSECURE": "true", + "OCM_OCM_STORAGE_PROVIDER_INSECURE": "true", + "NOTIFICATIONS_SMTP_HOST": EMAIL_SMTP_HOST, + "NOTIFICATIONS_SMTP_PORT": EMAIL_SMTP_PORT, + "NOTIFICATIONS_SMTP_INSECURE": "true", + "NOTIFICATIONS_SMTP_SENDER": EMAIL_SMTP_SENDER, + }, + }, + "authApp": { + "suites": ["apiAuthApp"], + "extraServerEnvironment": { + "OCIS_ADD_RUN_SERVICES": "auth-app", + "PROXY_ENABLE_APP_AUTH": "true", + }, + }, + "wopi": { + "suites": ["apiCollaboration"], + "collaborationServiceNeeded": True, + "extraServerEnvironment": { + "GATEWAY_GRPC_ADDR": "0.0.0.0:9142", + }, + }, + "cliCommands": { + "suites": ["cliCommands", "apiServiceAvailability"], + "antivirusNeeded": True, + "emailNeeded": True, + "extraEnvironment": { + "EMAIL_HOST": EMAIL_SMTP_HOST, + "EMAIL_PORT": EMAIL_PORT, + }, + "extraServerEnvironment": { + "NOTIFICATIONS_SMTP_HOST": EMAIL_SMTP_HOST, + "NOTIFICATIONS_SMTP_PORT": EMAIL_SMTP_PORT, + "NOTIFICATIONS_SMTP_INSECURE": "true", + "NOTIFICATIONS_SMTP_SENDER": EMAIL_SMTP_SENDER, + "NOTIFICATIONS_DEBUG_ADDR": "0.0.0.0:9174", + "ANTIVIRUS_SCANNER_TYPE": "clamav", + "ANTIVIRUS_CLAMAV_SOCKET": "tcp://clamav:3310", + "ANTIVIRUS_DEBUG_ADDR": "0.0.0.0:9277", + "OCIS_ADD_RUN_SERVICES": "antivirus,notifications", + }, + }, +} + +# reverse lookup: suite → group config +_SUITE_TO_CONFIG: dict = {} +for _cfg in LOCAL_API_TESTS.values(): + for _s in _cfg.get("suites", []): + _SUITE_TO_CONFIG[_s] = _cfg + + +def merged_config(suites: list) -> dict: + """Union config for all requested suites.""" + merged = { + "emailNeeded": False, + "antivirusNeeded": False, + "tikaNeeded": False, + "federationServer": False, + "collaborationServiceNeeded": False, + "extraServerEnvironment": {}, + "extraEnvironment": {}, + } + for suite in suites: + cfg = _SUITE_TO_CONFIG.get(suite, {}) + for flag in ("emailNeeded", "antivirusNeeded", "tikaNeeded", + "federationServer", "collaborationServiceNeeded"): + if cfg.get(flag): + merged[flag] = True + merged["extraServerEnvironment"].update(cfg.get("extraServerEnvironment", {})) + merged["extraEnvironment"].update(cfg.get("extraEnvironment", {})) + return merged + + +def base_server_env(repo_root: Path, ocis_url: str, ocis_config_dir: str) -> dict: + """Base ocis server environment matching drone ocisServer() function.""" + return { + "OCIS_URL": ocis_url, + "OCIS_CONFIG_DIR": ocis_config_dir, + "STORAGE_USERS_DRIVER": "ocis", + "PROXY_ENABLE_BASIC_AUTH": "true", + "OCIS_EXCLUDE_RUN_SERVICES": "idp", + "OCIS_LOG_LEVEL": "error", + "IDM_CREATE_DEMO_USERS": "true", + "IDM_ADMIN_PASSWORD": "admin", + "FRONTEND_SEARCH_MIN_LENGTH": "2", + "OCIS_ASYNC_UPLOADS": "true", + "OCIS_EVENTS_ENABLE_TLS": "false", + "NATS_NATS_HOST": "0.0.0.0", + "NATS_NATS_PORT": "9233", + "OCIS_JWT_SECRET": "some-ocis-jwt-secret", + "EVENTHISTORY_STORE": "memory", + "OCIS_TRANSLATION_PATH": str(repo_root / "tests/config/translations"), + "WEB_UI_CONFIG_FILE": str(repo_root / "tests/config/drone/ocis-config.json"), + "THUMBNAILS_TXT_FONTMAP_FILE": str(repo_root / "tests/config/drone/fontsMap.json"), + # default tika off (overridden by search2 extraServerEnvironment) + "SEARCH_EXTRACTOR_TYPE": "basic", + "FRONTEND_FULL_TEXT_SEARCH_ENABLED": "false", + # debug addresses + "ACTIVITYLOG_DEBUG_ADDR": "0.0.0.0:9197", + "APP_PROVIDER_DEBUG_ADDR": "0.0.0.0:9165", + "APP_REGISTRY_DEBUG_ADDR": "0.0.0.0:9243", + "AUTH_BASIC_DEBUG_ADDR": "0.0.0.0:9147", + "AUTH_MACHINE_DEBUG_ADDR": "0.0.0.0:9167", + "AUTH_SERVICE_DEBUG_ADDR": "0.0.0.0:9198", + "CLIENTLOG_DEBUG_ADDR": "0.0.0.0:9260", + "EVENTHISTORY_DEBUG_ADDR": "0.0.0.0:9270", + "FRONTEND_DEBUG_ADDR": "0.0.0.0:9141", + "GATEWAY_DEBUG_ADDR": "0.0.0.0:9143", + "GRAPH_DEBUG_ADDR": "0.0.0.0:9124", + "GROUPS_DEBUG_ADDR": "0.0.0.0:9161", + "IDM_DEBUG_ADDR": "0.0.0.0:9239", + "IDP_DEBUG_ADDR": "0.0.0.0:9134", + "INVITATIONS_DEBUG_ADDR": "0.0.0.0:9269", + "NATS_DEBUG_ADDR": "0.0.0.0:9234", + "OCDAV_DEBUG_ADDR": "0.0.0.0:9163", + "OCM_DEBUG_ADDR": "0.0.0.0:9281", + "OCS_DEBUG_ADDR": "0.0.0.0:9114", + "POSTPROCESSING_DEBUG_ADDR": "0.0.0.0:9255", + "PROXY_DEBUG_ADDR": "0.0.0.0:9205", + "SEARCH_DEBUG_ADDR": "0.0.0.0:9224", + "SETTINGS_DEBUG_ADDR": "0.0.0.0:9194", + "SHARING_DEBUG_ADDR": "0.0.0.0:9151", + "SSE_DEBUG_ADDR": "0.0.0.0:9139", + "STORAGE_PUBLICLINK_DEBUG_ADDR": "0.0.0.0:9179", + "STORAGE_SHARES_DEBUG_ADDR": "0.0.0.0:9156", + "STORAGE_SYSTEM_DEBUG_ADDR": "0.0.0.0:9217", + "STORAGE_USERS_DEBUG_ADDR": "0.0.0.0:9159", + "THUMBNAILS_DEBUG_ADDR": "0.0.0.0:9189", + "USERLOG_DEBUG_ADDR": "0.0.0.0:9214", + "USERS_DEBUG_ADDR": "0.0.0.0:9145", + "WEB_DEBUG_ADDR": "0.0.0.0:9104", + "WEBDAV_DEBUG_ADDR": "0.0.0.0:9119", + "WEBFINGER_DEBUG_ADDR": "0.0.0.0:9279", + } + + +def wait_for(condition_fn, timeout: int, label: str) -> None: + deadline = time.time() + timeout + while not condition_fn(): + if time.time() > deadline: + print(f"Timeout waiting for {label}", file=sys.stderr) + sys.exit(1) + time.sleep(1) + + +def ocis_healthy(ocis_url: str) -> bool: + r = subprocess.run( + ["curl", "-sk", "-uadmin:admin", + f"{ocis_url}/graph/v1.0/users/admin", + "-w", "%{http_code}", "-o", "/dev/null"], + capture_output=True, text=True, + ) + return r.stdout.strip() == "200" + + +def mailpit_healthy() -> bool: + return subprocess.run( + ["curl", "-sf", "http://localhost:8025/api/v1/messages"], + capture_output=True, + ).returncode == 0 + + +def tika_healthy() -> bool: + return subprocess.run( + ["curl", "-sf", "http://localhost:9998"], + capture_output=True, + ).returncode == 0 + + +def clamav_healthy() -> bool: + """Check ClamAV is ready by attempting a TCP connection to port 3310.""" + import socket + try: + with socket.create_connection(("localhost", 3310), timeout=2): + return True + except (ConnectionRefusedError, OSError): + return False + + +def run(cmd: list, env: dict = None, check: bool = True): + e = {**os.environ, **(env or {})} + return subprocess.run(cmd, env=e, check=check) + + +def main() -> int: + behat_suites_raw = os.environ.get("BEHAT_SUITES", "").strip() + if not behat_suites_raw: + print("BEHAT_SUITES is required", file=sys.stderr) + return 1 + + suites = [s.strip() for s in behat_suites_raw.split(",") if s.strip()] + acceptance_test_type = os.environ.get("ACCEPTANCE_TEST_TYPE", "api") + + repo_root = Path(__file__).resolve().parents[2] + ocis_bin = repo_root / "ocis/bin/ocis" + wrapper_bin = repo_root / "tests/ociswrapper/bin/ociswrapper" + ocis_url = "https://localhost:9200" + ocis_config_dir = Path.home() / ".ocis/config" + + cfg = merged_config(suites) + print(f"Suites: {suites}") + print(f"Services: email={cfg['emailNeeded']} tika={cfg['tikaNeeded']} antivirus={cfg['antivirusNeeded']}") + + # build + run(["make", "-C", str(repo_root / "ocis"), "build"]) + run(["make", "-C", str(repo_root / "tests/ociswrapper"), "build"], + env={"GOWORK": "off"}) + + # php deps + run(["composer", "install", "--no-progress"], + env={"COMPOSER_NO_INTERACTION": "1", "COMPOSER_NO_AUDIT": "1"}) + run(["composer", "bin", "behat", "install", "--no-progress"], + env={"COMPOSER_NO_INTERACTION": "1", "COMPOSER_NO_AUDIT": "1"}) + + # optional services + procs = [] + + if cfg["emailNeeded"]: + print("Starting mailpit...") + run(["docker", "run", "-d", "--name", "mailpit", "--network", "host", + "axllent/mailpit:v1.22.3"]) + wait_for(mailpit_healthy, 60, "mailpit") + print("mailpit ready.") + + if cfg["antivirusNeeded"]: + print("Starting clamav...") + run(["docker", "run", "-d", "--name", "clamav", "--network", "host", + "owncloudci/clamavd"]) + wait_for(clamav_healthy, 300, "clamav") + print("clamav ready.") + # override socket: drone uses container DNS "clamav", we use localhost + cfg["extraServerEnvironment"]["ANTIVIRUS_CLAMAV_SOCKET"] = "tcp://localhost:3310" + + if cfg["tikaNeeded"]: + print("Starting tika...") + run(["docker", "run", "-d", "--name", "tika", "--network", "host", + "apache/tika:3.2.2.0-full"]) + wait_for(tika_healthy, 120, "tika") + print("tika ready.") + + # init ocis + run([str(ocis_bin), "init", "--insecure", "true"]) + shutil.copy( + repo_root / "tests/config/drone/app-registry.yaml", + ocis_config_dir / "app-registry.yaml", + ) + + # assemble ocis server env + server_env = {**os.environ} + server_env.update(base_server_env(repo_root, ocis_url, str(ocis_config_dir))) + server_env.update(cfg["extraServerEnvironment"]) + + # start ociswrapper + print("Starting ocis...") + wrapper_proc = subprocess.Popen( + [str(wrapper_bin), "serve", + "--bin", str(ocis_bin), + "--url", ocis_url, + "--admin-username", "admin", + "--admin-password", "admin"], + env=server_env, + ) + procs.append(wrapper_proc) + + def cleanup(*_): + for p in procs: + try: + p.terminate() + except Exception: + pass + + signal.signal(signal.SIGTERM, cleanup) + signal.signal(signal.SIGINT, cleanup) + + try: + wait_for(lambda: ocis_healthy(ocis_url), 300, "ocis") + print("ocis ready.") + + # expected failures file + if acceptance_test_type == "core-api": + filter_tags = "~@skipOnGraph&&~@skipOnOcis-OCIS-Storage" + base_failures = repo_root / "tests/acceptance/expected-failures-API-on-OCIS-storage.md" + else: + filter_tags = "~@skip&&~@skipOnGraph&&~@skipOnOcis-OCIS-Storage" + base_failures = repo_root / "tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md" + + ef_override = os.environ.get("EXPECTED_FAILURES_FILE") + if ef_override: + p = Path(ef_override) + base_failures = p if p.is_absolute() else repo_root / p + + # merge expected-failures-without-remotephp.md (drone does this) + tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) + tmp.write(base_failures.read_text()) + without_rphp = repo_root / "tests/acceptance/expected-failures-without-remotephp.md" + if without_rphp.exists(): + tmp.write("\n") + tmp.write(without_rphp.read_text()) + tmp.close() + + # run tests + behat_env = { + **os.environ, + "TEST_SERVER_URL": ocis_url, + "OCIS_WRAPPER_URL": "http://localhost:5200", + "BEHAT_SUITES": behat_suites_raw, + "ACCEPTANCE_TEST_TYPE": acceptance_test_type, + "BEHAT_FILTER_TAGS": filter_tags, + "EXPECTED_FAILURES_FILE": tmp.name, + "STORAGE_DRIVER": "ocis", + "UPLOAD_DELETE_WAIT_TIME": "0", + "EMAIL_HOST": "localhost", + "EMAIL_PORT": EMAIL_PORT, + } + behat_env.update(cfg["extraEnvironment"]) + + print(f"Running suites: {behat_suites_raw} (type: {acceptance_test_type})") + result = subprocess.run( + ["make", "-C", str(repo_root), "test-acceptance-api"], + env=behat_env, + ) + return result.returncode + + finally: + cleanup() + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/tests/acceptance/run-github.sh b/tests/acceptance/run-github.sh index 469fd5bb12e..1ef9943b695 100755 --- a/tests/acceptance/run-github.sh +++ b/tests/acceptance/run-github.sh @@ -54,11 +54,23 @@ timeout 300 bash -c \ echo "ocis ready." # run acceptance tests for declared suites -echo "Running suites: $BEHAT_SUITES" +# ACCEPTANCE_TEST_TYPE: "api" (default) or "core-api" +ACCEPTANCE_TEST_TYPE="${ACCEPTANCE_TEST_TYPE:-api}" + +if [ "$ACCEPTANCE_TEST_TYPE" = "core-api" ]; then + _FILTER_TAGS="~@skipOnGraph&&~@skipOnOcis-OCIS-Storage" + _EXPECTED_FAILURES="${EXPECTED_FAILURES_FILE:-$REPO_ROOT/tests/acceptance/expected-failures-API-on-OCIS-storage.md}" +else + _FILTER_TAGS="~@skip&&~@skipOnGraph&&~@skipOnOcis-OCIS-Storage" + _EXPECTED_FAILURES="${EXPECTED_FAILURES_FILE:-$REPO_ROOT/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md}" +fi + +echo "Running suites: $BEHAT_SUITES (type: $ACCEPTANCE_TEST_TYPE)" TEST_SERVER_URL=$OCIS_URL \ OCIS_WRAPPER_URL=http://localhost:5200 \ BEHAT_SUITES=$BEHAT_SUITES \ -BEHAT_FILTER_TAGS="~@skip&&~@skipOnGraph&&~@skipOnOcis-OCIS-Storage" \ -EXPECTED_FAILURES_FILE="$REPO_ROOT/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md" \ +ACCEPTANCE_TEST_TYPE=$ACCEPTANCE_TEST_TYPE \ +BEHAT_FILTER_TAGS="$_FILTER_TAGS" \ +EXPECTED_FAILURES_FILE="$_EXPECTED_FAILURES" \ STORAGE_DRIVER=ocis \ make -C "$REPO_ROOT" test-acceptance-api diff --git a/tests/acceptance/run-litmus.sh b/tests/acceptance/run-litmus.sh new file mode 100644 index 00000000000..9722c25e651 --- /dev/null +++ b/tests/acceptance/run-litmus.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +OCIS_BIN="$REPO_ROOT/ocis/bin/ocis" +WRAPPER_BIN="$REPO_ROOT/tests/ociswrapper/bin/ociswrapper" +OCIS_URL="https://localhost:9200" +OCIS_CONFIG_DIR="$HOME/.ocis/config" + +# build +make -C "$REPO_ROOT/ocis" build +GOWORK=off make -C "$REPO_ROOT/tests/ociswrapper" build + +# init + start ocis +"$OCIS_BIN" init --insecure true +cp "$REPO_ROOT/tests/config/drone/app-registry.yaml" "$OCIS_CONFIG_DIR/app-registry.yaml" + +OCIS_URL=$OCIS_URL \ +OCIS_CONFIG_DIR=$OCIS_CONFIG_DIR \ +STORAGE_USERS_DRIVER=ocis \ +PROXY_ENABLE_BASIC_AUTH=true \ +OCIS_EXCLUDE_RUN_SERVICES=idp \ +OCIS_LOG_LEVEL=error \ +IDM_CREATE_DEMO_USERS=true \ +IDM_ADMIN_PASSWORD=admin \ +OCIS_ASYNC_UPLOADS=true \ +OCIS_EVENTS_ENABLE_TLS=false \ +NATS_NATS_HOST=0.0.0.0 \ +NATS_NATS_PORT=9233 \ +OCIS_JWT_SECRET=some-ocis-jwt-secret \ +WEB_UI_CONFIG_FILE="$REPO_ROOT/tests/config/drone/ocis-config.json" \ + "$WRAPPER_BIN" serve \ + --bin "$OCIS_BIN" \ + --url "$OCIS_URL" \ + --admin-username admin \ + --admin-password admin & +WRAPPER_PID=$! +trap "kill $WRAPPER_PID 2>/dev/null || true" EXIT + +echo "Waiting for ocis..." +timeout 300 bash -c \ + "while [ \$(curl -sk -uadmin:admin $OCIS_URL/graph/v1.0/users/admin \ + -w %{http_code} -o /dev/null) != 200 ]; do sleep 1; done" +echo "ocis ready." + +# setup: creates test folder, share, and exports SPACE_ID + PUBLIC_TOKEN to .env +TEST_SERVER_URL=$OCIS_URL bash "$REPO_ROOT/tests/config/drone/setup-for-litmus.sh" +source .env + +# run litmus against each WebDAV endpoint +ENDPOINTS=( + "$OCIS_URL/remote.php/webdav" + "$OCIS_URL/remote.php/dav/files/admin" + "$OCIS_URL/remote.php/dav/files/admin/Shares/new_folder/" + "$OCIS_URL/remote.php/webdav/Shares/new_folder/" + "$OCIS_URL/remote.php/dav/spaces/$SPACE_ID" +) + +for ENDPOINT in "${ENDPOINTS[@]}"; do + echo "Testing endpoint: $ENDPOINT" + docker run --rm --network host \ + -e LITMUS_URL="$ENDPOINT" \ + -e LITMUS_USERNAME=admin \ + -e LITMUS_PASSWORD=admin \ + -e TESTS="basic copymove props http" \ + owncloudci/litmus:latest \ + /usr/local/bin/litmus-wrapper +done