diff --git a/.github/actions/ocis-setup/action.yml b/.github/actions/ocis-setup/action.yml new file mode 100644 index 00000000000..8d489b02b6b --- /dev/null +++ b/.github/actions/ocis-setup/action.yml @@ -0,0 +1,226 @@ +name: ocis-setup +description: Start an oCIS instance with optional services (antivirus, email, tika, ociswrapper) + +inputs: + ocis-version: + description: oCIS release version (e.g. 8.0.1) or "latest" — ignored when ocis-binary is set + required: false + default: latest + ocis-binary: + description: Path to a pre-built oCIS binary; skips download when provided + required: false + default: "" + admin-password: + description: Admin user password + required: false + default: admin + log-level: + description: oCIS log level + required: false + default: error + demo-users: + description: Create demo users (IDM_CREATE_DEMO_USERS=true) + required: false + default: "false" + wrapper: + description: Start ociswrapper on :5200 for dynamic reconfiguration + required: false + default: "true" + tika: + description: Start Apache Tika on :9998 for full-text search + required: false + default: "false" + email: + description: Start Mailpit SMTP (:1025) and API (:8025) + required: false + default: "false" + antivirus: + description: Start ClamAV on :3310 and enable postprocessing + required: false + default: "false" + extra-server-env: + description: JSON object of additional env vars to pass to the oCIS server + required: false + default: "{}" + +outputs: + ocis-url: + description: oCIS instance URL + value: https://localhost:9200 + +runs: + using: composite + steps: + # ------------------------------------------------------------------------- + # 1. Install oCIS binary (skip when a pre-built binary is provided) + # ------------------------------------------------------------------------- + - name: Install oCIS binary from release + if: inputs.ocis-binary == '' + shell: bash + env: + OCIS_VERSION: ${{ inputs.ocis-version }} + run: ${{ github.action_path }}/scripts/install-ocis.sh + + - name: Use pre-built oCIS binary + if: inputs.ocis-binary != '' + shell: bash + run: | + sudo cp "${{ inputs.ocis-binary }}" /usr/local/bin/ocis + sudo chmod +x /usr/local/bin/ocis + echo "Using pre-built oCIS: $(ocis --version 2>&1 | head -1)" + + # ------------------------------------------------------------------------- + # 2. Build ociswrapper (always — it is always used as the process launcher) + # ------------------------------------------------------------------------- + - name: Build ociswrapper from source (when using release binary) + if: inputs.ocis-binary == '' + shell: bash + env: + OCIS_VERSION: ${{ inputs.ocis-version }} + run: ${{ github.action_path }}/scripts/install-wrapper.sh + + - name: Build ociswrapper from local source (when using pre-built binary) + if: inputs.ocis-binary != '' + shell: bash + working-directory: ${{ github.workspace }}/tests/ociswrapper + run: | + if command -v ociswrapper &>/dev/null; then + echo "ociswrapper already installed: $(ociswrapper --version 2>&1 | head -1)" + else + GOWORK=off go build -o /tmp/ociswrapper . + sudo mv /tmp/ociswrapper /usr/local/bin/ociswrapper + echo "ociswrapper built from local source." + fi + + # ------------------------------------------------------------------------- + # 3. Optional services + # ------------------------------------------------------------------------- + - name: Start Mailpit (email) + if: inputs.email == 'true' + shell: bash + run: | + docker run -d --name mailpit --network host axllent/mailpit:v1.22.3 + timeout 60 bash -c 'until curl -sf http://localhost:8025/api/v1/messages; do sleep 1; done' + echo "mailpit ready." + + - name: Start ClamAV (antivirus) + if: inputs.antivirus == 'true' + shell: bash + run: | + docker run -d --name clamav --network host owncloudci/clamavd + echo "Waiting for ClamAV on :3310 (may take up to 5 min for virus DB download)..." + timeout 300 bash -c ' + while true; do + if (echo "" | nc -z localhost 3310) 2>/dev/null || \ + python3 -c "import socket,sys; s=socket.create_connection((\"localhost\",3310),2); s.close()" 2>/dev/null; then + break + fi + sleep 2 + done + ' + echo "clamav ready." + + - name: Start Tika (full-text search) + if: inputs.tika == 'true' + shell: bash + run: | + docker run -d --name tika --network host apache/tika:3.2.2.0-full + timeout 120 bash -c 'until curl -sf http://localhost:9998; do sleep 2; done' + echo "tika ready." + + # ------------------------------------------------------------------------- + # 4. Install test-runner dependencies (must happen before oCIS starts — + # apt-get pulls in libcurl4-openssl-dev which replaces the system libcurl + # and would kill the running oCIS process if installed afterwards) + # ------------------------------------------------------------------------- + - name: Install libcurl 8.12.0 build dependencies + shell: bash + run: | + sudo apt-get update -qq + sudo NEEDRESTART_MODE=a apt-get install -y \ + libssl-dev libnghttp2-dev libpsl-dev libldap-dev libssh-dev zlib1g-dev libvips-dev + + - name: Cache libcurl 8.12.0 + id: cache-libcurl + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: /opt/libcurl + key: libcurl-8.12.0-${{ runner.os }} + + - name: Compile libcurl 8.12.0 from source + if: steps.cache-libcurl.outputs.cache-hit != 'true' + shell: bash + run: | + 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=/opt/libcurl \ + --enable-versioned-symbols --silent + make -j$(nproc) --silent + sudo make install --silent + + - name: Restore libcurl ldconfig + shell: bash + run: | + echo "/opt/libcurl/lib" | sudo tee /etc/ld.so.conf.d/libcurl-8.conf + sudo ldconfig + /opt/libcurl/bin/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); + } + ' + + - name: Install Composer dependencies + shell: bash + working-directory: ${{ github.workspace }} + run: | + COMPOSER_NO_INTERACTION=1 COMPOSER_NO_AUDIT=1 composer install --no-progress + COMPOSER_NO_INTERACTION=1 COMPOSER_NO_AUDIT=1 \ + composer bin behat install --no-progress + + # ------------------------------------------------------------------------- + # 6. Init oCIS + copy config files + # ------------------------------------------------------------------------- + - name: Init oCIS + shell: bash + env: + OCIS_ACTION_PATH: ${{ github.action_path }} + run: ${{ github.action_path }}/scripts/init-ocis.sh + + # ------------------------------------------------------------------------- + # 7. Start oCIS via ociswrapper + # ------------------------------------------------------------------------- + - name: Start oCIS + shell: bash + env: + ADMIN_PASSWORD: ${{ inputs.admin-password }} + LOG_LEVEL: ${{ inputs.log-level }} + DEMO_USERS: ${{ inputs.demo-users }} + ANTIVIRUS_ENABLED: ${{ inputs.antivirus }} + EMAIL_ENABLED: ${{ inputs.email }} + TIKA_ENABLED: ${{ inputs.tika }} + EXTRA_SERVER_ENV: ${{ inputs.extra-server-env }} + OCIS_REPO_ROOT: ${{ github.workspace }} + run: ${{ github.action_path }}/scripts/start-ocis.sh + + # ------------------------------------------------------------------------- + # 8. Wait for oCIS to be healthy + # ------------------------------------------------------------------------- + - name: Wait for oCIS + shell: bash + run: | + echo "Waiting for oCIS at https://localhost:9200..." + timeout 300 bash -c ' + until curl -sk -uadmin:admin https://localhost:9200/graph/v1.0/users/admin \ + -w "%{http_code}" -o /dev/null 2>/dev/null | grep -q "200"; do + sleep 2 + done + ' || (echo "=== oCIS failed to start ===" && \ + cat /tmp/ocis-server.log 2>/dev/null | tail -50 && exit 1) + echo "ocis ready." + diff --git a/.github/actions/ocis-setup/config/app-registry.yaml b/.github/actions/ocis-setup/config/app-registry.yaml new file mode 100644 index 00000000000..a1042ec45fb --- /dev/null +++ b/.github/actions/ocis-setup/config/app-registry.yaml @@ -0,0 +1,9 @@ +app_registry: + mimetypes: + - mime_type: application/vnd.oasis.opendocument.text + extension: odt + name: OpenDocument + description: OpenDocument text document + icon: "" + default_app: FakeOffice + allow_creation: true diff --git a/.github/actions/ocis-setup/scripts/init-ocis.sh b/.github/actions/ocis-setup/scripts/init-ocis.sh new file mode 100755 index 00000000000..47744b56aae --- /dev/null +++ b/.github/actions/ocis-setup/scripts/init-ocis.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Initialize oCIS config directory and copy required config files. +# OCIS_ACTION_PATH: path to the action directory (contains config/) +# The action is used from within a repo checkout so GITHUB_WORKSPACE is set. + +CONFIG_DIR="${HOME}/.ocis/config" +mkdir -p "$CONFIG_DIR" + +ocis init --insecure true + +# app-registry.yaml: bundled with this action so it works without a repo checkout +cp "${OCIS_ACTION_PATH}/config/app-registry.yaml" "${CONFIG_DIR}/app-registry.yaml" + +echo "oCIS config initialized at ${CONFIG_DIR}" diff --git a/.github/actions/ocis-setup/scripts/install-ocis.sh b/.github/actions/ocis-setup/scripts/install-ocis.sh new file mode 100755 index 00000000000..d70606f1181 --- /dev/null +++ b/.github/actions/ocis-setup/scripts/install-ocis.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +# OCIS_VERSION: "latest" or a specific version like "8.0.1" +VERSION="${OCIS_VERSION:-latest}" + +if [[ "$VERSION" == "latest" ]]; then + VERSION=$(curl -s https://api.github.com/repos/owncloud/ocis/releases/latest \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'].lstrip('v'))") +fi + +# Write resolved version so install-wrapper.sh uses the same tag without a second API call. +echo "$VERSION" > /tmp/ocis-resolved-version + +echo "Installing oCIS $VERSION..." +ARCH=$(uname -m) +case "$ARCH" in + x86_64) ARCH_SUFFIX="amd64" ;; + aarch64) ARCH_SUFFIX="arm64" ;; + *) echo "Unsupported arch: $ARCH"; exit 1 ;; +esac + +BASE_URL="https://github.com/owncloud/ocis/releases/download/v${VERSION}" +BINARY="ocis-${VERSION}-linux-${ARCH_SUFFIX}" + +curl -sLo /tmp/ocis "${BASE_URL}/${BINARY}" +curl -sLo /tmp/ocis.sha256 "${BASE_URL}/${BINARY}.sha256" + +# sha256 file contains "HASH filename" — rewrite to match our local path +HASH=$(awk '{print $1}' /tmp/ocis.sha256) +echo "${HASH} /tmp/ocis" | sha256sum -c - + +chmod +x /tmp/ocis +sudo mv /tmp/ocis /usr/local/bin/ocis + +echo "oCIS $(ocis --version 2>&1 | head -1) installed." diff --git a/.github/actions/ocis-setup/scripts/install-wrapper.sh b/.github/actions/ocis-setup/scripts/install-wrapper.sh new file mode 100755 index 00000000000..c5e4af473c6 --- /dev/null +++ b/.github/actions/ocis-setup/scripts/install-wrapper.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ociswrapper is not published as a release artifact. +# Sparse-clone only tests/ociswrapper/ from the matching tag and build it. +VERSION="${OCIS_VERSION:-latest}" + +if [[ "$VERSION" == "latest" ]]; then + if [[ -f /tmp/ocis-resolved-version ]]; then + VERSION=$(cat /tmp/ocis-resolved-version) + else + VERSION=$(curl -s https://api.github.com/repos/owncloud/ocis/releases/latest \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'].lstrip('v'))") + fi +fi + +CLONE_DIR="/tmp/ocis-src-wrapper" +rm -rf "$CLONE_DIR" + +echo "Sparse-cloning ociswrapper from tag v${VERSION}..." +git clone --depth=1 --filter=blob:none --sparse \ + --branch "v${VERSION}" \ + https://github.com/owncloud/ocis.git "$CLONE_DIR" + +cd "$CLONE_DIR" +git sparse-checkout set tests/ociswrapper + +cd tests/ociswrapper +echo "Building ociswrapper..." +GOWORK=off go build -o /tmp/ociswrapper . +sudo mv /tmp/ociswrapper /usr/local/bin/ociswrapper + +echo "ociswrapper installed at $(which ociswrapper)" +rm -rf "$CLONE_DIR" diff --git a/.github/actions/ocis-setup/scripts/start-ocis.sh b/.github/actions/ocis-setup/scripts/start-ocis.sh new file mode 100755 index 00000000000..63eb7dd55ba --- /dev/null +++ b/.github/actions/ocis-setup/scripts/start-ocis.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Start oCIS via ociswrapper. +# ociswrapper wraps the ocis binary and exposes HTTP API on :5200 +# for dynamic reconfiguration (env var changes at runtime). +# +# Environment variables set by action.yml: +# ADMIN_PASSWORD, LOG_LEVEL, DEMO_USERS +# ANTIVIRUS_ENABLED, EMAIL_ENABLED, TIKA_ENABLED +# EXTRA_SERVER_ENV (JSON object) +# OCIS_REPO_ROOT (GITHUB_WORKSPACE — needed for config file paths) + +OCIS_URL="https://localhost:9200" +CONFIG_DIR="${HOME}/.ocis/config" + +# Generate a throw-away fontsMap.json pointing at the font shipped in the repo. +# Tests that render thumbnails from text files need this. +REPO_ROOT="${OCIS_REPO_ROOT:-${GITHUB_WORKSPACE}}" +FONT_PATH="${REPO_ROOT}/tests/config/ci/NotoSans.ttf" +FONTMAP=$(mktemp /tmp/fontsMap-XXXXXX.json) +echo "{\"defaultFont\": \"${FONT_PATH}\"}" > "$FONTMAP" + +declare -A SERVER_ENV=( + [OCIS_URL]="$OCIS_URL" + [OCIS_CONFIG_DIR]="$CONFIG_DIR" + [STORAGE_USERS_DRIVER]="ocis" + [PROXY_ENABLE_BASIC_AUTH]="true" + [OCIS_LOG_LEVEL]="${LOG_LEVEL:-error}" + [IDM_CREATE_DEMO_USERS]="${DEMO_USERS:-false}" + [IDM_ADMIN_PASSWORD]="${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" + [MICRO_REGISTRY_ADDRESS]="127.0.0.1:9233" + [OCIS_JWT_SECRET]="some-ocis-jwt-secret" + [EVENTHISTORY_STORE]="memory" + [OCIS_TRANSLATION_PATH]="${REPO_ROOT}/tests/config/translations" + [WEB_UI_CONFIG_FILE]="${REPO_ROOT}/tests/config/ci/ocis-config.json" + [THUMBNAILS_TXT_FONTMAP_FILE]="$FONTMAP" + [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" +) + +# Antivirus +if [[ "${ANTIVIRUS_ENABLED:-false}" == "true" ]]; then + SERVER_ENV[ANTIVIRUS_SCANNER_TYPE]="clamav" + SERVER_ENV[ANTIVIRUS_CLAMAV_SOCKET]="tcp://localhost:3310" + SERVER_ENV[POSTPROCESSING_STEPS]="virusscan" + SERVER_ENV[OCIS_ADD_RUN_SERVICES]="antivirus" + SERVER_ENV[ANTIVIRUS_DEBUG_ADDR]="0.0.0.0:9277" +fi + +# Email (notifications service) +if [[ "${EMAIL_ENABLED:-false}" == "true" ]]; then + SERVER_ENV[OCIS_ADD_RUN_SERVICES]="${SERVER_ENV[OCIS_ADD_RUN_SERVICES]:+${SERVER_ENV[OCIS_ADD_RUN_SERVICES]},}notifications" + SERVER_ENV[NOTIFICATIONS_SMTP_HOST]="localhost" + SERVER_ENV[NOTIFICATIONS_SMTP_PORT]="1025" + SERVER_ENV[NOTIFICATIONS_SMTP_INSECURE]="true" + SERVER_ENV[NOTIFICATIONS_SMTP_SENDER]="ownCloud " + SERVER_ENV[NOTIFICATIONS_DEBUG_ADDR]="0.0.0.0:9174" +fi + +# Tika (full-text search) +if [[ "${TIKA_ENABLED:-false}" == "true" ]]; then + SERVER_ENV[FRONTEND_FULL_TEXT_SEARCH_ENABLED]="true" + SERVER_ENV[SEARCH_EXTRACTOR_TYPE]="tika" + SERVER_ENV[SEARCH_EXTRACTOR_TIKA_TIKA_URL]="http://localhost:9998" + SERVER_ENV[SEARCH_EXTRACTOR_CS3SOURCE_INSECURE]="true" +fi + +# Extra env vars from JSON input — use null-delimited records to handle values with '=' or newlines +if [[ -n "${EXTRA_SERVER_ENV:-}" && "${EXTRA_SERVER_ENV}" != "{}" ]]; then + while IFS=$'\x01' read -r -d $'\x00' key val; do + SERVER_ENV["$key"]="$val" + done < <(echo "$EXTRA_SERVER_ENV" | python3 -c " +import sys, json +d = json.load(sys.stdin) +for k, v in d.items(): + sys.stdout.buffer.write(k.encode() + b'\x01' + v.encode() + b'\x00') +") +fi + +# Build env for the subprocess +ENV_ARGS=() +for key in "${!SERVER_ENV[@]}"; do + ENV_ARGS+=("${key}=${SERVER_ENV[$key]}") +done + +echo "Starting ociswrapper + oCIS server..." +env "${ENV_ARGS[@]}" ociswrapper serve \ + --bin /usr/local/bin/ocis \ + --url "$OCIS_URL" \ + --admin-username admin \ + --admin-password "${ADMIN_PASSWORD:-admin}" \ + > /tmp/ocis-server.log 2>&1 & + +echo $! > /tmp/ocis-wrapper.pid +echo "ociswrapper started (PID $(cat /tmp/ocis-wrapper.pid)), log: /tmp/ocis-server.log" diff --git a/.github/actions/ocis-start-direct/action.yml b/.github/actions/ocis-start-direct/action.yml new file mode 100644 index 00000000000..188f13844cc --- /dev/null +++ b/.github/actions/ocis-start-direct/action.yml @@ -0,0 +1,142 @@ +name: ocis-start-direct +description: > + Start oCIS directly (ocis server, no ociswrapper) with Docker bridge IP as OCIS_URL + so Docker containers can reach the host-side oCIS process. + +inputs: + ocis-binary: + description: Path to a pre-built oCIS binary + required: true + demo-users: + description: Create demo users (IDM_CREATE_DEMO_USERS) + required: false + default: "false" + exclude-services: + description: OCIS_EXCLUDE_RUN_SERVICES value + required: false + default: "idp" + extra-server-env: + description: JSON object of additional env vars to pass to the oCIS server + required: false + default: "{}" + +outputs: + bridge-ip: + description: Docker bridge gateway IP (reachable from host and containers) + value: ${{ steps.bridge.outputs.bridge-ip }} + ocis-url: + description: oCIS URL using bridge IP + value: ${{ steps.bridge.outputs.ocis-url }} + +runs: + using: composite + steps: + - name: Install oCIS binary + shell: bash + run: | + sudo cp "${{ inputs.ocis-binary }}" /usr/local/bin/ocis + sudo chmod +x /usr/local/bin/ocis + + - name: Get Docker bridge IP + id: bridge + shell: bash + run: | + BRIDGE_IP=$(docker network inspect bridge \ + --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}') + echo "bridge-ip=${BRIDGE_IP}" >> "$GITHUB_OUTPUT" + echo "ocis-url=https://${BRIDGE_IP}:9200" >> "$GITHUB_OUTPUT" + echo "OCIS_BRIDGE_IP=${BRIDGE_IP}" >> "$GITHUB_ENV" + echo "Docker bridge IP: ${BRIDGE_IP}" + + - name: Init oCIS + shell: bash + env: + EXTRA_SERVER_ENV: ${{ inputs.extra-server-env }} + DEMO_USERS: ${{ inputs.demo-users }} + EXCLUDE_SERVICES: ${{ inputs.exclude-services }} + WORKSPACE: ${{ github.workspace }} + run: | + CONFIG_DIR="${HOME}/.ocis/config" + mkdir -p "$CONFIG_DIR" + OCIS_URL="https://${OCIS_BRIDGE_IP}:9200" + + EXTRA_ENV=( + "OCIS_URL=${OCIS_URL}" + "OCIS_CONFIG_DIR=${CONFIG_DIR}" + "STORAGE_USERS_DRIVER=ocis" + "PROXY_ENABLE_BASIC_AUTH=true" + "OCIS_EXCLUDE_RUN_SERVICES=${EXCLUDE_SERVICES}" + "OCIS_LOG_LEVEL=error" + "IDM_CREATE_DEMO_USERS=${DEMO_USERS}" + "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" + "WEB_UI_CONFIG_FILE=${WORKSPACE}/tests/config/ci/ocis-config.json" + ) + + if [[ -n "$EXTRA_SERVER_ENV" && "$EXTRA_SERVER_ENV" != "{}" ]]; then + while IFS= read -r pair; do + EXTRA_ENV+=("$pair") + done < <(echo "$EXTRA_SERVER_ENV" | jq -r 'to_entries[] | "\(.key)=\(.value)"') + fi + + env "${EXTRA_ENV[@]}" ocis init --insecure true + cp "${WORKSPACE}/tests/config/ci/app-registry.yaml" "${CONFIG_DIR}/app-registry.yaml" + + - name: Start oCIS server + shell: bash + env: + EXTRA_SERVER_ENV: ${{ inputs.extra-server-env }} + DEMO_USERS: ${{ inputs.demo-users }} + EXCLUDE_SERVICES: ${{ inputs.exclude-services }} + WORKSPACE: ${{ github.workspace }} + run: | + CONFIG_DIR="${HOME}/.ocis/config" + OCIS_URL="https://${OCIS_BRIDGE_IP}:9200" + + EXTRA_ENV=( + "OCIS_URL=${OCIS_URL}" + "OCIS_CONFIG_DIR=${CONFIG_DIR}" + "STORAGE_USERS_DRIVER=ocis" + "PROXY_ENABLE_BASIC_AUTH=true" + "OCIS_EXCLUDE_RUN_SERVICES=${EXCLUDE_SERVICES}" + "OCIS_LOG_LEVEL=error" + "IDM_CREATE_DEMO_USERS=${DEMO_USERS}" + "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" + "WEB_UI_CONFIG_FILE=${WORKSPACE}/tests/config/ci/ocis-config.json" + ) + + if [[ -n "$EXTRA_SERVER_ENV" && "$EXTRA_SERVER_ENV" != "{}" ]]; then + while IFS= read -r pair; do + EXTRA_ENV+=("$pair") + done < <(echo "$EXTRA_SERVER_ENV" | jq -r 'to_entries[] | "\(.key)=\(.value)"') + fi + + env "${EXTRA_ENV[@]}" ocis server > /tmp/ocis-direct.log 2>&1 & + echo $! > /tmp/ocis-direct.pid + echo "oCIS started (PID $(cat /tmp/ocis-direct.pid)), log: /tmp/ocis-direct.log" + + - name: Wait for oCIS + shell: bash + run: | + echo "Waiting for oCIS at https://localhost:9200..." + timeout 300 bash -c ' + until curl -sk -uadmin:admin https://localhost:9200/graph/v1.0/users/admin \ + -w "%{http_code}" -o /dev/null 2>/dev/null | grep -q "200"; do + sleep 2 + done + ' || (echo "=== oCIS failed to start ===" && \ + cat /tmp/ocis-direct.log 2>/dev/null | tail -50 && exit 1) + echo "ocis ready." diff --git a/.github/actions/ocis-test/action.yml b/.github/actions/ocis-test/action.yml new file mode 100644 index 00000000000..516ddaeffd4 --- /dev/null +++ b/.github/actions/ocis-test/action.yml @@ -0,0 +1,44 @@ +name: ocis-test +description: Run oCIS Behat acceptance tests against a running oCIS instance + +inputs: + suite: + description: Behat suite(s) to run, comma-separated (e.g. apiGraph or apiAntivirus,apiSettings) + required: true + ocis-url: + description: oCIS instance URL + required: false + default: https://localhost:9200 + expected-failures-file: + description: Path to expected failures markdown (relative to repo root) + required: false + default: "" + acceptance-test-type: + description: Test type passed as ACCEPTANCE_TEST_TYPE (api or core-api) + required: false + default: api + with-remote-php: + description: Include remote.php expected failures (WITH_REMOTE_PHP) + required: false + default: "false" + +runs: + using: composite + steps: + - name: Run ${{ inputs.suite }} + shell: bash + working-directory: ${{ github.workspace }} + env: + BEHAT_SUITES: ${{ inputs.suite }} + TEST_SERVER_URL: ${{ inputs.ocis-url }} + OCIS_WRAPPER_URL: http://localhost:5200 + ACCEPTANCE_TEST_TYPE: ${{ inputs.acceptance-test-type }} + WITH_REMOTE_PHP: ${{ inputs.with-remote-php }} + EXPECTED_FAILURES_FILE: ${{ inputs.expected-failures-file }} + STORAGE_DRIVER: ocis + UPLOAD_DELETE_WAIT_TIME: "0" + EMAIL_HOST: localhost + EMAIL_PORT: "8025" + # Filter tags mirror run-github.py logic + BEHAT_FILTER_TAGS: ${{ inputs.acceptance-test-type == 'core-api' && '~@skipOnGraph&&~@skipOnOcis-OCIS-Storage' || '~@skip&&~@skipOnGraph&&~@skipOnOcis-OCIS-Storage' }} + run: make test-acceptance-api diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 8922594708a..50ebf584b7a 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -94,7 +94,31 @@ jobs: exit $FAILED - local-api-tests: + - name: Build ociswrapper + run: make -C tests/ociswrapper build GOWORK=off + + - name: Upload oCIS binary + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: ocis-binary + path: ocis/bin/ocis + retention-days: 1 + if-no-files-found: error + + - name: Upload ociswrapper binary + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: ociswrapper-binary + path: tests/ociswrapper/bin/ociswrapper + retention-days: 1 + if-no-files-found: error + + # --------------------------------------------------------------------------- + # API acceptance tests — split by required services so each group gets a + # single fixed ocis-setup call with no per-suite conditionals. + # --------------------------------------------------------------------------- + + api-tests-plain: name: ${{ matrix.suite }} needs: [build-and-test] runs-on: ubuntu-latest @@ -105,10 +129,6 @@ jobs: # contract & locks - apiContract - apiLocks - # settings & notifications (needs email) - - apiSettings - - apiNotification - - apiCors # graph - apiGraphUser - apiGraph @@ -122,10 +142,8 @@ jobs: - apiDepthInfinity - apiArchiver - apiActivities - # search + # search (no tika) - apiSearch1 - - apiSearch2 - - apiSearchContent # needs Tika # sharing - apiSharingNgShares - apiReshare @@ -136,80 +154,538 @@ jobs: - 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 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 with: - go-version-file: go.mod - cache: true + php-version: "8.4" + extensions: curl, xml, mbstring, zip, ldap, gd + tools: composer + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + - name: Merge expected failures files + run: | + cat tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md \ + tests/acceptance/expected-failures-without-remotephp.md \ + > /tmp/expected-failures.md + - name: Test ${{ matrix.suite }} + uses: ./.github/actions/ocis-test + with: + suite: ${{ matrix.suite }} + expected-failures-file: /tmp/expected-failures.md + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + api-tests-email: + name: ${{ matrix.suite }} + needs: [build-and-test] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + suite: + # settings & notifications (needs Mailpit) + - apiSettings + - apiNotification + - apiCors + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 with: - node-version: "24" + php-version: "8.4" + extensions: curl, xml, mbstring, zip, ldap, gd + tools: composer + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + email: "true" + - name: Merge expected failures files + run: | + cat tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md \ + tests/acceptance/expected-failures-without-remotephp.md \ + > /tmp/expected-failures.md + - name: Test ${{ matrix.suite }} + uses: ./.github/actions/ocis-test + with: + suite: ${{ matrix.suite }} + expected-failures-file: /tmp/expected-failures.md + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi - - name: Enable pnpm - run: corepack enable && corepack prepare pnpm@10.28.1 --activate + api-tests-tika: + name: ${{ matrix.suite }} + needs: [build-and-test] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + suite: + # full-text search (needs Tika) + - apiSearch2 + - apiSearchContent + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 + with: + php-version: "8.4" + extensions: curl, xml, mbstring, zip, ldap, gd + tools: composer + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + tika: "true" + - name: Merge expected failures files + run: | + cat tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md \ + tests/acceptance/expected-failures-without-remotephp.md \ + > /tmp/expected-failures.md + - name: Test ${{ matrix.suite }} + uses: ./.github/actions/ocis-test + with: + suite: ${{ matrix.suite }} + expected-failures-file: /tmp/expected-failures.md + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi + + api-tests-antivirus: + name: apiAntivirus + needs: [build-and-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 + with: + php-version: "8.4" + extensions: curl, xml, mbstring, zip, ldap, gd + tools: composer + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + antivirus: "true" + - name: Merge expected failures files + run: | + cat tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md \ + tests/acceptance/expected-failures-without-remotephp.md \ + > /tmp/expected-failures.md + - name: Test apiAntivirus + uses: ./.github/actions/ocis-test + with: + suite: apiAntivirus + expected-failures-file: /tmp/expected-failures.md + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi + api-tests-auth-app: + name: apiAuthApp + needs: [build-and-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 with: php-version: "8.4" extensions: curl, xml, mbstring, zip, ldap, gd tools: composer + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + extra-server-env: '{"OCIS_ADD_RUN_SERVICES":"auth-app","PROXY_ENABLE_APP_AUTH":"true"}' + - name: Merge expected failures files + run: | + cat tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md \ + tests/acceptance/expected-failures-without-remotephp.md \ + > /tmp/expected-failures.md + - name: Test apiAuthApp + uses: ./.github/actions/ocis-test + with: + suite: apiAuthApp + expected-failures-file: /tmp/expected-failures.md + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi - - name: Cache libcurl 8.12.0 - id: cache-libcurl - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + api-tests-ocm: + name: apiOcm + needs: [build-and-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 with: - path: /opt/libcurl - key: libcurl-8.12.0-${{ runner.os }} - - - name: Install libcurl 8.12.0 build dependencies - # Always run: libvips-dev is needed at runtime (govips thumbnails) and for - # pkg-config --exists vips to succeed so oCIS is built with ENABLE_VIPS=true. - # Skipping this on cache hit would cause oCIS to fall back to the imaging library. - run: | - sudo apt-get update -qq - # NEEDRESTART_MODE=a: auto-restart services silently; without this, needrestart can fail with exit code 6 - sudo NEEDRESTART_MODE=a apt-get install -y libssl-dev libnghttp2-dev libpsl-dev libldap-dev libssh-dev zlib1g-dev libvips-dev - - - name: Compile libcurl 8.12.0 from source - if: steps.cache-libcurl.outputs.cache-hit != 'true' - run: | - 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=/opt/libcurl --enable-versioned-symbols --silent - make -j$(nproc) --silent - sudo make install --silent - - - name: Restore libcurl ldconfig - # Always run: on cache hit the .so files are restored to /opt/libcurl/lib but - # /etc/ld.so.cache on the fresh runner doesn't know about them yet. - # Without ldconfig, PHP's curl extension can't find libcurl even though it's present. - run: | - echo "/opt/libcurl/lib" | sudo tee /etc/ld.so.conf.d/libcurl-8.conf - sudo ldconfig - /opt/libcurl/bin/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); - } - ' + php-version: "8.4" + extensions: curl, xml, mbstring, zip, ldap, gd + tools: composer + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Rewrite providers.json for localhost + run: | + sed \ + -e 's|ocis-server:9200|localhost:9200|g' \ + -e 's|federation-ocis-server:10200|localhost:10200|g' \ + tests/config/ci/providers.json > /tmp/ocm-providers.json + echo "providers.json rewritten to /tmp/ocm-providers.json" + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + email: "true" + extra-server-env: >- + {"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", + "OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE":"/tmp/ocm-providers.json", + "NOTIFICATIONS_SMTP_HOST":"localhost","NOTIFICATIONS_SMTP_PORT":"1025", + "NOTIFICATIONS_SMTP_INSECURE":"true", + "NOTIFICATIONS_SMTP_SENDER":"ownCloud ", + "NOTIFICATIONS_DEBUG_ADDR":"0.0.0.0:9174"} + - name: Start federation oCIS + run: | + FED_CONFIG_DIR="${HOME}/.ocis-federation/config" + FED_DATA_DIR="${HOME}/.ocis-federation" + mkdir -p "$FED_CONFIG_DIR" + + # Build federation env from .env-federation (source of port/addr truth) + FED_ENV=() + while IFS= read -r line; do + line="${line#export }" + line="${line#\!}" + [[ -z "$line" || "$line" == \#* ]] && continue + FED_ENV+=("$line") + done < tests/config/local/.env-federation + + # CI overrides + FED_ENV+=("OCIS_URL=https://localhost:10200") + FED_ENV+=("OCIS_BASE_DATA_PATH=${FED_DATA_DIR}") + FED_ENV+=("OCIS_CONFIG_DIR=${FED_CONFIG_DIR}") + FED_ENV+=("OCIS_RUNTIME_PORT=10250") + FED_ENV+=("MICRO_REGISTRY_ADDRESS=127.0.0.1:10233") + FED_ENV+=("NATS_NATS_PORT=10233") + FED_ENV+=("OCIS_EXCLUDE_RUN_SERVICES=idp") + FED_ENV+=("STORAGE_USERS_DRIVER=ocis") + FED_ENV+=("PROXY_ENABLE_BASIC_AUTH=true") + FED_ENV+=("IDM_CREATE_DEMO_USERS=true") + FED_ENV+=("IDM_ADMIN_PASSWORD=admin") + FED_ENV+=("OCIS_EVENTS_ENABLE_TLS=false") + FED_ENV+=("OCIS_JWT_SECRET=some-ocis-jwt-secret") + FED_ENV+=("EVENTHISTORY_STORE=memory") + FED_ENV+=("OCIS_ADD_RUN_SERVICES=ocm,notifications") + FED_ENV+=("OCIS_ENABLE_OCM=true") + FED_ENV+=("OCM_OCM_INVITE_MANAGER_INSECURE=true") + FED_ENV+=("OCM_OCM_SHARE_PROVIDER_INSECURE=true") + FED_ENV+=("OCM_OCM_STORAGE_PROVIDER_INSECURE=true") + FED_ENV+=("OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE=/tmp/ocm-providers.json") + FED_ENV+=("NOTIFICATIONS_SMTP_HOST=localhost") + FED_ENV+=("NOTIFICATIONS_SMTP_PORT=1025") + FED_ENV+=("NOTIFICATIONS_SMTP_INSECURE=true") + FED_ENV+=("NOTIFICATIONS_SMTP_SENDER=ownCloud ") + + env "${FED_ENV[@]}" ocis/bin/ocis init --insecure true --config-path "$FED_CONFIG_DIR" + cp tests/config/ci/app-registry.yaml "$FED_CONFIG_DIR/app-registry.yaml" + + env "${FED_ENV[@]}" ocis/bin/ocis server > /tmp/ocis-federation.log 2>&1 & + echo $! > /tmp/ocis-federation.pid + echo "federation oCIS started (PID $(cat /tmp/ocis-federation.pid))" + + echo "Waiting for federation oCIS at https://localhost:10200..." + timeout 300 bash -c ' + until curl -sk -uadmin:admin https://localhost:10200/graph/v1.0/users/admin \ + -w "%{http_code}" -o /dev/null 2>/dev/null | grep -q "200"; do + sleep 2 + done + ' || (echo "=== federation oCIS failed ===" && cat /tmp/ocis-federation.log | tail -50 && exit 1) + echo "federation ocis ready." + - name: Merge expected failures files + run: | + cat tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md \ + tests/acceptance/expected-failures-without-remotephp.md \ + > /tmp/expected-failures.md + - name: Test apiOcm + uses: ./.github/actions/ocis-test + with: + suite: apiOcm + expected-failures-file: /tmp/expected-failures.md + env: + TEST_SERVER_FED_URL: https://localhost:10200 + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi + if [[ -f /tmp/ocis-federation.pid ]]; then + kill "$(cat /tmp/ocis-federation.pid)" 2>/dev/null || true + rm -f /tmp/ocis-federation.pid /tmp/ocis-federation.log + fi + + api-tests-wopi: + name: apiCollaboration + needs: [build-and-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 + with: + php-version: "8.4" + extensions: curl, xml, mbstring, zip, ldap, gd + tools: composer + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Start fakeoffice + run: | + docker run -d --name fakeoffice --network host \ + -v "$(pwd):/ocis:ro" \ + python:3-alpine \ + python3 /ocis/tests/config/ci/fakeoffice-server.py + - name: Start collabora + run: | + docker run -d --name collabora --network host \ + -e DONT_GEN_SSL_CERT=set \ + -e "extra_params=--o:ssl.enable=true --o:ssl.termination=true --o:welcome.enable=false --o:net.frame_ancestors=https://localhost:9200" \ + --entrypoint /bin/sh \ + collabora/code:24.04.5.1.1 \ + -c "set -e; coolconfig generate-proof-key; bash /start-collabora-online.sh" + - name: Start onlyoffice + run: | + sudo systemctl stop postgresql || true + docker run -d --name onlyoffice --network host \ + -e WOPI_ENABLED=true \ + -e USE_UNAUTHORIZED_STORAGE=true \ + -v "$(pwd)/tests/config/ci/only-office.json:/tmp/only-office.json:ro" \ + -v "$(pwd)/tests/config/ci/onlyoffice-entrypoint.sh:/entrypoint.sh:ro" \ + --entrypoint /bin/sh \ + onlyoffice/documentserver:9.0.0 \ + /entrypoint.sh + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + extra-server-env: '{"GATEWAY_GRPC_ADDR":"0.0.0.0:9142"}' + - name: Wait for WOPI app discovery endpoints + run: | + echo "Waiting for fakeoffice /hosting/discovery..." + timeout 300 bash -c 'until curl -sf http://localhost:8080/hosting/discovery; do sleep 2; done' + echo "Waiting for collabora /hosting/discovery..." + timeout 300 bash -c 'until curl -sfk https://localhost:9980/hosting/discovery; do sleep 2; done' + echo "Waiting for onlyoffice /hosting/discovery..." + timeout 300 bash -c 'until curl -sfk https://localhost:443/hosting/discovery; do sleep 2; done' + echo "All WOPI apps ready." + - name: Start collaboration services + run: | + CONFIG_DIR="${HOME}/.ocis/config" + OCIS_BIN="$(pwd)/ocis/bin/ocis" + + declare -A PORTS_COLLAB=( + [collabora_grpc]=9301 [collabora_http]=9300 [collabora_debug]=9304 + [onlyoffice_grpc]=9311 [onlyoffice_http]=9310 [onlyoffice_debug]=9314 + [fakeoffice_grpc]=9321 [fakeoffice_http]=9320 [fakeoffice_debug]=9324 + ) - - name: Run ${{ matrix.suite }} - run: BEHAT_SUITES=${{ matrix.suite }} python3 tests/acceptance/run-github.py + start_collab() { + local name=$1 app_name=$2 product=$3 addr=$4 grpc=$5 http=$6 debug=$7 + env \ + OCIS_URL="https://localhost:9200" \ + OCIS_CONFIG_DIR="$CONFIG_DIR" \ + MICRO_REGISTRY=nats-js-kv \ + MICRO_REGISTRY_ADDRESS="localhost:9233" \ + COLLABORATION_LOG_LEVEL=debug \ + COLLABORATION_GRPC_ADDR="0.0.0.0:${grpc}" \ + COLLABORATION_HTTP_ADDR="0.0.0.0:${http}" \ + COLLABORATION_DEBUG_ADDR="0.0.0.0:${debug}" \ + COLLABORATION_APP_PROOF_DISABLE=true \ + COLLABORATION_APP_INSECURE=true \ + COLLABORATION_CS3API_DATAGATEWAY_INSECURE=true \ + OCIS_JWT_SECRET=some-ocis-jwt-secret \ + COLLABORATION_WOPI_SECRET=some-wopi-secret \ + COLLABORATION_APP_NAME="$app_name" \ + COLLABORATION_APP_PRODUCT="$product" \ + COLLABORATION_APP_ADDR="$addr" \ + COLLABORATION_WOPI_SRC="http://localhost:${http}" \ + "$OCIS_BIN" collaboration server > "/tmp/collab-${name}.log" 2>&1 & + echo $! > "/tmp/collab-${name}.pid" + echo "collaboration-${name} started (PID $!)" + } + + start_collab collabora Collabora Collabora \ + "https://localhost:9980" \ + ${PORTS_COLLAB[collabora_grpc]} ${PORTS_COLLAB[collabora_http]} ${PORTS_COLLAB[collabora_debug]} + start_collab onlyoffice OnlyOffice OnlyOffice \ + "https://localhost:443" \ + ${PORTS_COLLAB[onlyoffice_grpc]} ${PORTS_COLLAB[onlyoffice_http]} ${PORTS_COLLAB[onlyoffice_debug]} + start_collab fakeoffice FakeOffice Microsoft \ + "http://localhost:8080" \ + ${PORTS_COLLAB[fakeoffice_grpc]} ${PORTS_COLLAB[fakeoffice_http]} ${PORTS_COLLAB[fakeoffice_debug]} + + # Wait for all three healthz endpoints + for name in collabora onlyoffice fakeoffice; do + key="${name}_debug" + port="${PORTS_COLLAB[$key]}" + echo "Waiting for collaboration-${name} healthz on :${port}..." + timeout 300 bash -c "until curl -sf http://localhost:${port}/healthz; do sleep 2; done" \ + || (echo "=== collab-${name} log ===" && cat /tmp/collab-${name}.log | tail -30 && exit 1) + done + echo "All collaboration services ready." + - name: Merge expected failures files + run: | + cat tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md \ + tests/acceptance/expected-failures-without-remotephp.md \ + > /tmp/expected-failures.md + - name: Test apiCollaboration + uses: ./.github/actions/ocis-test + with: + suite: apiCollaboration + expected-failures-file: /tmp/expected-failures.md + env: + COLLABORATION_SERVICE_URL: http://localhost:9320 + - name: Stop oCIS and collaboration services + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + fi + for name in collabora onlyoffice fakeoffice; do + if [[ -f "/tmp/collab-${name}.pid" ]]; then + kill "$(cat /tmp/collab-${name}.pid)" 2>/dev/null || true + fi + done cli-tests: needs: [build-and-test] @@ -222,26 +698,61 @@ jobs: - cliCommands,apiServiceAvailability # grouped: both need ClamAV + email services steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - cache: true - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: "24" - - - name: Enable pnpm - run: corepack enable && corepack prepare pnpm@10.28.1 --activate - - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 with: php-version: "8.4" extensions: curl, xml, mbstring, zip, ldap, gd tools: composer - - - name: Run ${{ matrix.suite }} - run: BEHAT_SUITES="${{ matrix.suite }}" python3 tests/acceptance/run-github.py + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Start ClamAV + run: | + docker run -d --name clamav --network host owncloudci/clamavd + echo "Waiting for ClamAV on :3310..." + timeout 300 bash -c ' + while true; do + python3 -c "import socket,sys; s=socket.create_connection((\"localhost\",3310),2); s.close()" 2>/dev/null && break + sleep 2 + done + ' + echo "clamav ready." + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + email: "true" + extra-server-env: '{"OCIS_ADD_RUN_SERVICES":"antivirus,notifications","ANTIVIRUS_SCANNER_TYPE":"clamav","ANTIVIRUS_CLAMAV_SOCKET":"tcp://localhost:3310","ANTIVIRUS_DEBUG_ADDR":"0.0.0.0:9277"}' + - name: Merge expected failures files + run: | + cat tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md \ + tests/acceptance/expected-failures-without-remotephp.md \ + > /tmp/expected-failures.md + - name: Test ${{ matrix.suite }} + uses: ./.github/actions/ocis-test + with: + suite: ${{ matrix.suite }} + expected-failures-file: /tmp/expected-failures.md + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi core-api-tests: name: ${{ matrix.suite }} @@ -262,74 +773,48 @@ jobs: - "coreApiWebdavMove1,coreApiWebdavPreviews,coreApiWebdavUpload,coreApiWebdavUploadTUS" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - cache: true - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: "24" - - - name: Enable pnpm - run: corepack enable && corepack prepare pnpm@10.28.1 --activate - - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 with: php-version: "8.4" extensions: curl, xml, mbstring, zip, ldap, gd tools: composer - - - name: Cache libcurl 8.12.0 - id: cache-libcurl - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: - path: /opt/libcurl - key: libcurl-8.12.0-${{ runner.os }} - - - name: Install libcurl 8.12.0 build dependencies - # Always run: libvips-dev is needed at runtime (govips thumbnails) and for - # pkg-config --exists vips to succeed so oCIS is built with ENABLE_VIPS=true. - # Skipping this on cache hit would cause oCIS to fall back to the imaging library. - run: | - sudo apt-get update -qq - # NEEDRESTART_MODE=a: auto-restart services silently; without this, needrestart can fail with exit code 6 - sudo NEEDRESTART_MODE=a apt-get install -y libssl-dev libnghttp2-dev libpsl-dev libldap-dev libssh-dev zlib1g-dev libvips-dev - - - name: Compile libcurl 8.12.0 from source - if: steps.cache-libcurl.outputs.cache-hit != 'true' - run: | - 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=/opt/libcurl --enable-versioned-symbols --silent - make -j$(nproc) --silent - sudo make install --silent - - - name: Restore libcurl ldconfig - # Always run: on cache hit the .so files are restored to /opt/libcurl/lib but - # /etc/ld.so.cache on the fresh runner doesn't know about them yet. - # Without ldconfig, PHP's curl extension can't find libcurl even though it's present. - run: | - echo "/opt/libcurl/lib" | sudo tee /etc/ld.so.conf.d/libcurl-8.conf - sudo ldconfig - /opt/libcurl/bin/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); - } - ' - - - name: Run ${{ matrix.suite }} - run: > - BEHAT_SUITES="${{ matrix.suite }}" - ACCEPTANCE_TEST_TYPE=core-api - WITH_REMOTE_PHP=true - python3 tests/acceptance/run-github.py + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: /usr/local/bin + - name: Mark ociswrapper binary executable + run: chmod +x /usr/local/bin/ociswrapper + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + - name: Merge expected failures files + run: | + cp tests/acceptance/expected-failures-API-on-OCIS-storage.md /tmp/expected-failures.md + - name: Test ${{ matrix.suite }} + uses: ./.github/actions/ocis-test + with: + suite: ${{ matrix.suite }} + acceptance-test-type: core-api + with-remote-php: "true" + expected-failures-file: /tmp/expected-failures.md + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi e2e-tests: name: e2e-${{ matrix.suite }} @@ -355,21 +840,25 @@ jobs: keycloak: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - cache: true - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" - name: Enable pnpm run: corepack enable && corepack prepare pnpm@10.28.1 --activate - - name: Generate code + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ocis-binary + path: ocis/bin + - name: Download ociswrapper binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ociswrapper-binary + path: tests/ociswrapper/bin + - name: Mark binaries executable run: | - pnpm config set store-dir ./.pnpm-store - make ci-node-generate - env: - CHROMEDRIVER_SKIP_DOWNLOAD: "true" + chmod +x ocis/bin/ocis + chmod +x tests/ociswrapper/bin/ociswrapper - name: Cache Playwright Chromium uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 @@ -436,11 +925,190 @@ jobs: || (echo "=== keycloak logs ===" && docker logs keycloak --tail 80 && exit 1) echo "keycloak ready." + - name: Clone web repo + run: | + WEB_BRANCH=$(grep '^WEB_BRANCH=' ci.env | cut -d= -f2- | tr -d '[:space:]') + WEB_COMMITID=$(grep '^WEB_COMMITID=' ci.env | cut -d= -f2- | tr -d '[:space:]' || true) + WEB_BRANCH="${WEB_BRANCH:-master}" + git clone -b "$WEB_BRANCH" --single-branch --no-tags \ + https://github.com/owncloud/web.git webTestRunner + if [[ -n "$WEB_COMMITID" ]]; then + git -C webTestRunner checkout "$WEB_COMMITID" + fi + + - name: Install web dependencies and Playwright + run: | + pnpm config set store-dir ./.pnpm-store + pnpm install + pnpm exec playwright install --with-deps chromium + working-directory: webTestRunner + + - name: Init oCIS + run: | + CONFIG_DIR="${HOME}/.ocis/config" + mkdir -p "$CONFIG_DIR" + ocis/bin/ocis init --insecure true + cp tests/config/ci/app-registry.yaml "$CONFIG_DIR/app-registry.yaml" + + - name: Trust oCIS self-signed cert + run: | + CERT="${HOME}/.ocis/proxy/server.crt" + if [[ -f "$CERT" ]]; then + sudo cp "$CERT" /usr/local/share/ca-certificates/ocis.crt + sudo update-ca-certificates + fi + + - name: Patch web UI config + run: | + sed 's|https://ocis-server:9200|https://127.0.0.1:9200|g' \ + tests/config/ci/ocis-config.json \ + > "${HOME}/.ocis/config/web-ui-config.json" + echo "web config written to ${HOME}/.ocis/config/web-ui-config.json" + + - name: Start oCIS + run: | + CONFIG_DIR="${HOME}/.ocis/config" + OCIS_URL="https://127.0.0.1:9200" + + SERVER_ENV=( + "OCIS_URL=${OCIS_URL}" + "OCIS_CONFIG_DIR=${CONFIG_DIR}" + "STORAGE_USERS_DRIVER=ocis" + "PROXY_ENABLE_BASIC_AUTH=true" + "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=$(pwd)/tests/config/translations" + "WEB_UI_CONFIG_FILE=${HOME}/.ocis/config/web-ui-config.json" + "THUMBNAILS_TXT_FONTMAP_FILE=$(pwd)/tests/config/ci/fontsMap.json" + "OCIS_PASSWORD_POLICY_BANNED_PASSWORDS_LIST=$(pwd)/tests/config/ci/banned-password-list.txt" + "GRAPH_AVAILABLE_ROLES=b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5,a8d5fe5e-96e3-418d-825b-534dbdf22b99,fb6c3e19-e378-47e5-b277-9732f9de6e21,58c63c02-1d89-4572-916a-870abc5a1b7d,2d00ce52-1fc2-4dbc-8b95-a73b73395f5a,1c996275-f1c9-4e71-abdf-a42f6495e960,312c0871-5ef7-4b3a-85b6-0e4074c64049,aa97fe03-7980-45ac-9e50-b325749fd7e6,63e64e19-8d43-42ec-a738-2b6af2610efa" + "FRONTEND_CONFIGURABLE_NOTIFICATIONS=true" + "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" + ) + + if [[ "${{ matrix.tika }}" == "true" ]]; then + SERVER_ENV+=( + "FRONTEND_FULL_TEXT_SEARCH_ENABLED=true" + "SEARCH_EXTRACTOR_TYPE=tika" + "SEARCH_EXTRACTOR_TIKA_TIKA_URL=http://localhost:9998" + "SEARCH_EXTRACTOR_CS3SOURCE_INSECURE=true" + ) + fi + + if [[ "${{ matrix.keycloak }}" == "true" ]]; then + SERVER_ENV+=( + "OCIS_EXCLUDE_RUN_SERVICES=idp" + "PROXY_AUTOPROVISION_ACCOUNTS=true" + "PROXY_ROLE_ASSIGNMENT_DRIVER=oidc" + "OCIS_OIDC_ISSUER=https://localhost:8443/realms/oCIS" + "PROXY_OIDC_REWRITE_WELLKNOWN=true" + "WEB_OIDC_CLIENT_ID=web" + "PROXY_USER_OIDC_CLAIM=preferred_username" + "PROXY_USER_CS3_CLAIM=username" + "OCIS_ADMIN_USER_ID=" + "GRAPH_ASSIGN_DEFAULT_USER_ROLE=false" + "GRAPH_USERNAME_MATCH=none" + "PROXY_CSP_CONFIG_FILE_LOCATION=$(pwd)/tests/config/ci/csp.yaml" + "KEYCLOAK_DOMAIN=localhost:8443" + "IDM_CREATE_DEMO_USERS=false" + ) + env "${SERVER_ENV[@]}" ocis/bin/ocis server > /tmp/ocis-e2e.log 2>&1 & + else + env "${SERVER_ENV[@]}" tests/ociswrapper/bin/ociswrapper serve \ + --bin ocis/bin/ocis \ + --url "$OCIS_URL" \ + --admin-username admin \ + --admin-password admin \ + > /tmp/ocis-e2e.log 2>&1 & + fi + echo $! > /tmp/ocis-e2e.pid + echo "oCIS started (PID $(cat /tmp/ocis-e2e.pid))" + + - name: Wait for oCIS + run: | + if [[ "${{ matrix.keycloak }}" == "true" ]]; then + # Keycloak mode: no local admin user, check openid-configuration + timeout 300 bash -c ' + until curl -sk https://127.0.0.1:9200/.well-known/openid-configuration \ + -w "%{http_code}" -o /dev/null 2>/dev/null | grep -q "200"; do + sleep 2 + done + ' || (echo "=== oCIS failed ===" && cat /tmp/ocis-e2e.log | tail -50 && exit 1) + else + timeout 300 bash -c ' + until curl -sk -uadmin:admin https://127.0.0.1:9200/graph/v1.0/users/admin \ + -w "%{http_code}" -o /dev/null 2>/dev/null | grep -q "200"; do + sleep 2 + done + ' || (echo "=== oCIS failed ===" && cat /tmp/ocis-e2e.log | tail -50 && exit 1) + fi + echo "ocis ready." + + - name: Set cert path env + run: echo "OCIS_CERT=${HOME}/.ocis/proxy/server.crt" >> "$GITHUB_ENV" + - name: Run e2e-${{ matrix.suite }} - run: E2E_ARGS="${{ matrix.args }}" python3 tests/acceptance/run-e2e.py + run: bash run-e2e.sh ${{ matrix.args }} + working-directory: webTestRunner/tests/e2e env: - TIKA_NEEDED: ${{ matrix.tika == true && 'true' || 'false' }} - KEYCLOAK_NEEDED: ${{ matrix.keycloak == true && 'true' || 'false' }} + BASE_URL_OCIS: https://127.0.0.1:9200 + HEADLESS: "true" + RETRY: "3" + SKIP_A11Y_TESTS: "true" + REPORT_TRACING: "true" + NODE_EXTRA_CA_CERTS: ${{ env.OCIS_CERT }} + BROWSER: chromium + KEYCLOAK: ${{ matrix.keycloak == true && 'true' || 'false' }} + KEYCLOAK_HOST: localhost:8443 + + - name: Stop oCIS + if: always() + run: | + if [[ -f /tmp/ocis-e2e.pid ]]; then + kill "$(cat /tmp/ocis-e2e.pid)" 2>/dev/null || true + fi litmus: name: litmus @@ -448,12 +1116,87 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: - go-version-file: go.mod - cache: true - - name: Run litmus - run: python3 tests/acceptance/run-litmus.py + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Start oCIS + uses: ./.github/actions/ocis-start-direct + id: ocis + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + - name: Setup for litmus + run: | + OCIS_URL="https://127.0.0.1:9200" + LITMUS_BASE="https://${{ steps.ocis.outputs.bridge-ip }}:9200" + + # Get personal space ID + SPACE_ID=$(curl -sk -uadmin:admin "${OCIS_URL}/graph/v1.0/me/drives" | python3 -c "import json,sys; drives=json.load(sys.stdin); [print([p for p in d.get('root',{}).get('webDavUrl','').split('/') if p][-1]) for d in drives.get('value',[]) if d.get('driveType')=='personal']") + echo "SPACE_ID=${SPACE_ID}" >> "$GITHUB_ENV" + echo "LITMUS_BASE=${LITMUS_BASE}" >> "$GITHUB_ENV" + echo "Space ID: ${SPACE_ID}" + + # Create test folder as einstein + curl -sk -ueinstein:relativity -X MKCOL \ + "${OCIS_URL}/remote.php/webdav/new_folder" > /dev/null + + # Share einstein → admin + SHARE_RESP=$(curl -sk -ueinstein:relativity \ + "${OCIS_URL}/ocs/v2.php/apps/files_sharing/api/v1/shares" \ + -d "path=/new_folder&shareType=0&permissions=15&name=new_folder&shareWith=admin") + SHARE_ID=$(echo "$SHARE_RESP" | python3 -c "import re,sys; m=re.search(r'(.+?)', sys.stdin.read()); print(m.group(1) if m else '')") + if [[ -n "$SHARE_ID" ]]; then + curl -X POST -sk -uadmin:admin \ + "${OCIS_URL}/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/${SHARE_ID}" \ + > /dev/null + fi + + # Public share + curl -sk -ueinstein:relativity \ + "${OCIS_URL}/ocs/v2.php/apps/files_sharing/api/v1/shares" \ + -d "path=/new_folder&shareType=3&permissions=15&name=new_folder" > /dev/null + + - name: Run litmus tests + run: | + LITMUS_IMAGE="owncloudci/litmus:latest" + BRIDGE_IP="${OCIS_BRIDGE_IP}" + BASE="https://${BRIDGE_IP}:9200" + SPACE_ID="${SPACE_ID}" + FAILED=() + + run_litmus() { + local name=$1 endpoint=$2 + echo "Testing [${name}]: ${endpoint}" + docker run --rm \ + -e "LITMUS_URL=${endpoint}" \ + -e LITMUS_USERNAME=admin \ + -e LITMUS_PASSWORD=admin \ + -e "TESTS=basic copymove props http" \ + "$LITMUS_IMAGE" || FAILED+=("$name") + } + + run_litmus "old-endpoint" "${BASE}/remote.php/webdav" + run_litmus "new-endpoint" "${BASE}/remote.php/dav/files/admin" + run_litmus "new-shared" "${BASE}/remote.php/dav/files/admin/Shares/new_folder/" + run_litmus "old-shared" "${BASE}/remote.php/webdav/Shares/new_folder/" + run_litmus "spaces-endpoint" "${BASE}/remote.php/dav/spaces/${SPACE_ID}" + + if [[ ${#FAILED[@]} -gt 0 ]]; then + echo "Failed: ${FAILED[*]}" + exit 1 + fi + echo "All litmus tests passed." + + - name: Stop oCIS + if: always() + run: | + if [[ -f /tmp/ocis-direct.pid ]]; then + kill "$(cat /tmp/ocis-direct.pid)" 2>/dev/null || true + fi cs3api: name: cs3api @@ -461,12 +1204,35 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: - go-version-file: go.mod - cache: true + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Start oCIS + uses: ./.github/actions/ocis-start-direct + id: ocis + with: + ocis-binary: ocis/bin/ocis + demo-users: "true" + extra-server-env: >- + {"GATEWAY_GRPC_ADDR":"0.0.0.0:9142", + "OCIS_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD":"false"} - name: Run cs3api validator - run: python3 tests/acceptance/run-cs3api.py + run: | + docker run --rm \ + --entrypoint /usr/bin/cs3api-validator \ + owncloud/cs3api-validator:0.2.1 \ + /var/lib/cs3api-validator \ + --endpoint="${OCIS_BRIDGE_IP}:9142" + - name: Stop oCIS + if: always() + run: | + if [[ -f /tmp/ocis-direct.pid ]]; then + kill "$(cat /tmp/ocis-direct.pid)" 2>/dev/null || true + fi wopi-builtin: name: wopi-builtin @@ -474,12 +1240,142 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: - go-version-file: go.mod - cache: true - - name: Run WOPI validator (builtin) - run: python3 tests/acceptance/run-wopi.py --type builtin + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Start fakeoffice + run: | + docker run -d --name wopi-fakeoffice \ + -p 8080:8080 \ + -v "$(pwd):/ocis:ro" \ + python:3-alpine \ + python3 /ocis/tests/config/ci/fakeoffice-server.py + - name: Get Docker bridge IP + id: bridge + run: | + BRIDGE_IP=$(docker network inspect bridge \ + --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}') + echo "ip=${BRIDGE_IP}" >> "$GITHUB_OUTPUT" + echo "OCIS_BRIDGE_IP=${BRIDGE_IP}" >> "$GITHUB_ENV" + - name: Start oCIS + uses: ./.github/actions/ocis-start-direct + id: ocis + with: + ocis-binary: ocis/bin/ocis + exclude-services: "idp,app-provider" + extra-server-env: >- + {"GATEWAY_GRPC_ADDR":"0.0.0.0:9142", + "APP_PROVIDER_EXTERNAL_ADDR":"com.owncloud.api.app-provider", + "APP_PROVIDER_DRIVER":"wopi", + "APP_PROVIDER_WOPI_APP_NAME":"FakeOffice", + "APP_PROVIDER_WOPI_APP_URL":"http://${{ steps.bridge.outputs.ip }}:8080", + "APP_PROVIDER_WOPI_INSECURE":"true", + "APP_PROVIDER_WOPI_WOPI_SERVER_EXTERNAL_URL":"http://${{ steps.bridge.outputs.ip }}:9300", + "APP_PROVIDER_WOPI_FOLDER_URL_BASE_URL":"https://${{ steps.bridge.outputs.ip }}:9200"} + - name: Wait for fakeoffice discovery + run: | + timeout 300 bash -c "until curl -sf http://localhost:8080/hosting/discovery; do sleep 2; done" + echo "fakeoffice discovery ready." + - name: Start collaboration service + run: | + BRIDGE_IP="${OCIS_BRIDGE_IP}" + CONFIG_DIR="${HOME}/.ocis/config" + env \ + OCIS_URL="https://${BRIDGE_IP}:9200" \ + OCIS_CONFIG_DIR="$CONFIG_DIR" \ + MICRO_REGISTRY=nats-js-kv \ + MICRO_REGISTRY_ADDRESS="127.0.0.1:9233" \ + COLLABORATION_LOG_LEVEL=debug \ + COLLABORATION_GRPC_ADDR="0.0.0.0:9301" \ + COLLABORATION_HTTP_ADDR="0.0.0.0:9300" \ + COLLABORATION_DEBUG_ADDR="0.0.0.0:9304" \ + COLLABORATION_APP_PROOF_DISABLE=true \ + COLLABORATION_APP_INSECURE=true \ + COLLABORATION_CS3API_DATAGATEWAY_INSECURE=true \ + OCIS_JWT_SECRET=some-ocis-jwt-secret \ + COLLABORATION_WOPI_SECRET=some-wopi-secret \ + COLLABORATION_APP_NAME=FakeOffice \ + COLLABORATION_APP_PRODUCT=Microsoft \ + COLLABORATION_APP_ADDR="http://${BRIDGE_IP}:8080" \ + COLLABORATION_WOPI_SRC="http://${BRIDGE_IP}:9300" \ + ocis/bin/ocis collaboration server > /tmp/collab-wopi.log 2>&1 & + echo $! > /tmp/collab-wopi.pid + echo "Waiting for collaboration service healthz on :9304..." + timeout 120 bash -c 'until curl -sf http://localhost:9304/healthz; do sleep 2; done' \ + || (cat /tmp/collab-wopi.log | tail -30 && exit 1) + echo "collaboration service ready." + - name: Prepare test file and run WOPI validator + run: | + BRIDGE_IP="${OCIS_BRIDGE_IP}" + OCIS_URL="https://127.0.0.1:9200" + VALIDATOR_IMAGE="owncloudci/wopi-validator" + + # PUT test file + curl -sk -uadmin:admin -X PUT \ + --fail --retry-connrefused --retry 7 --retry-all-errors \ + "${OCIS_URL}/remote.php/webdav/test.wopitest" \ + -D /tmp/wopi-headers.txt + + # Extract file ID + FILE_ID=$(grep -i 'Oc-Fileid:' /tmp/wopi-headers.txt | sed 's/.*: *//' | tr -d '\r\n ') + echo "FILE_ID=${FILE_ID}" + + # Open app + OPEN=$(curl -sk -uadmin:admin -X POST --fail \ + --retry-connrefused --retry 7 --retry-all-errors \ + "${OCIS_URL}/app/open?app_name=FakeOffice&file_id=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote('${FILE_ID}', safe=''))")") + echo "open: ${OPEN:0:400}" + + ACCESS_TOKEN=$(echo "$OPEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['form_parameters']['access_token'])") + TTL=$(echo "$OPEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['form_parameters']['access_token_ttl'])") + APP_URL=$(echo "$OPEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('app_url',''))") + if echo "$APP_URL" | grep -q 'files%2F'; then + FILE_ID_ENC=$(echo "$APP_URL" | sed 's/.*files%2F//') + elif echo "$APP_URL" | grep -q 'files/'; then + FILE_ID_ENC=$(echo "$APP_URL" | sed 's/.*files\///') + else + FILE_ID_ENC=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote('${FILE_ID}', safe=''))") + fi + WOPI_SRC="http://${BRIDGE_IP}:9300/wopi/files/${FILE_ID_ENC}" + echo "WOPI_SRC=${WOPI_SRC}" + + FAILED=() + SHARED_GROUPS=(BaseWopiViewing CheckFileInfoSchema EditFlows Locks AccessTokens GetLock ExtendedLockLength FileVersion Features) + BUILTIN_GROUPS=(PutRelativeFile RenameFileIfCreateChildFileIsNotSupported) + + for group in "${SHARED_GROUPS[@]}"; do + echo "Running testgroup [${group}]" + docker run --rm --workdir /app \ + --entrypoint /app/Microsoft.Office.WopiValidator \ + "$VALIDATOR_IMAGE" \ + -t "$ACCESS_TOKEN" -w "$WOPI_SRC" -l "$TTL" --testgroup "$group" \ + || FAILED+=("$group") + done + + for group in "${BUILTIN_GROUPS[@]}"; do + echo "Running testgroup [${group}] (secure)" + docker run --rm --workdir /app \ + --entrypoint /app/Microsoft.Office.WopiValidator \ + "$VALIDATOR_IMAGE" \ + -s -t "$ACCESS_TOKEN" -w "$WOPI_SRC" -l "$TTL" --testgroup "$group" \ + || FAILED+=("$group") + done + + if [[ ${#FAILED[@]} -gt 0 ]]; then + echo "Failed testgroups: ${FAILED[*]}" + exit 1 + fi + echo "All WOPI validator tests passed." + - name: Stop oCIS and collaboration service + if: always() + run: | + if [[ -f /tmp/ocis-direct.pid ]]; then kill "$(cat /tmp/ocis-direct.pid)" 2>/dev/null || true; fi + if [[ -f /tmp/collab-wopi.pid ]]; then kill "$(cat /tmp/collab-wopi.pid)" 2>/dev/null || true; fi + docker rm -f wopi-fakeoffice 2>/dev/null || true wopi-cs3: name: wopi-cs3 @@ -487,15 +1383,119 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - name: Download oCIS binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: - go-version-file: go.mod - cache: true - - name: Run WOPI validator (cs3) - run: python3 tests/acceptance/run-wopi.py --type cs3 + name: ocis-binary + path: ocis/bin + - name: Mark oCIS binary executable + run: chmod +x ocis/bin/ocis + - name: Start fakeoffice + run: | + docker run -d --name wopi-fakeoffice \ + -p 8080:8080 \ + -v "$(pwd):/ocis:ro" \ + python:3-alpine \ + python3 /ocis/tests/config/ci/fakeoffice-server.py + - name: Get Docker bridge IP + id: bridge + run: | + BRIDGE_IP=$(docker network inspect bridge \ + --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}') + echo "ip=${BRIDGE_IP}" >> "$GITHUB_OUTPUT" + echo "OCIS_BRIDGE_IP=${BRIDGE_IP}" >> "$GITHUB_ENV" + - name: Start oCIS + uses: ./.github/actions/ocis-start-direct + id: ocis + with: + ocis-binary: ocis/bin/ocis + extra-server-env: >- + {"GATEWAY_GRPC_ADDR":"0.0.0.0:9142", + "APP_PROVIDER_EXTERNAL_ADDR":"com.owncloud.api.app-provider", + "APP_PROVIDER_DRIVER":"wopi", + "APP_PROVIDER_WOPI_APP_NAME":"FakeOffice", + "APP_PROVIDER_WOPI_APP_URL":"http://${{ steps.bridge.outputs.ip }}:8080", + "APP_PROVIDER_WOPI_INSECURE":"true", + "APP_PROVIDER_WOPI_WOPI_SERVER_EXTERNAL_URL":"http://${{ steps.bridge.outputs.ip }}:9300", + "APP_PROVIDER_WOPI_FOLDER_URL_BASE_URL":"https://${{ steps.bridge.outputs.ip }}:9200"} + - name: Wait for fakeoffice discovery + run: | + timeout 300 bash -c "until curl -sf http://localhost:8080/hosting/discovery; do sleep 2; done" + echo "fakeoffice discovery ready." + - name: Start cs3 wopi server + run: | + BRIDGE_IP="${OCIS_BRIDGE_IP}" + sed "s|ocis-server|${BRIDGE_IP}|g" tests/config/ci/wopiserver.conf > /tmp/wopiserver-patched.conf + echo "123" > /tmp/wopisecret + + docker run -d --name wopi-cs3server \ + -p 9300:9300 \ + -v "/tmp/wopiserver-patched.conf:/etc/wopi/wopiserver.conf" \ + -v "/tmp/wopisecret:/etc/wopi/wopisecret" \ + --entrypoint /app/wopiserver.py \ + cs3org/wopiserver:v10.4.0 + + echo "Waiting for cs3 wopi server on :9300..." + timeout 120 bash -c " + until python3 -c \"import socket,sys; s=socket.create_connection(('${BRIDGE_IP}',9300),2); s.close()\" 2>/dev/null; do sleep 2; done + " + echo "cs3 wopi server ready." + - name: Prepare test file and run WOPI validator + run: | + BRIDGE_IP="${OCIS_BRIDGE_IP}" + OCIS_URL="https://127.0.0.1:9200" + VALIDATOR_IMAGE="owncloudci/wopi-validator" + + curl -sk -uadmin:admin -X PUT \ + --fail --retry-connrefused --retry 7 --retry-all-errors \ + "${OCIS_URL}/remote.php/webdav/test.wopitest" \ + -D /tmp/wopi-headers.txt + + FILE_ID=$(grep -i 'Oc-Fileid:' /tmp/wopi-headers.txt | sed 's/.*: *//' | tr -d '\r\n ') + echo "FILE_ID=${FILE_ID}" + + OPEN=$(curl -sk -uadmin:admin -X POST --fail \ + --retry-connrefused --retry 7 --retry-all-errors \ + "${OCIS_URL}/app/open?app_name=FakeOffice&file_id=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote('${FILE_ID}', safe=''))")") + + ACCESS_TOKEN=$(echo "$OPEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['form_parameters']['access_token'])") + TTL=$(echo "$OPEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['form_parameters']['access_token_ttl'])") + APP_URL=$(echo "$OPEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('app_url',''))") + if echo "$APP_URL" | grep -q 'files%2F'; then + FILE_ID_ENC=$(echo "$APP_URL" | sed 's/.*files%2F//') + elif echo "$APP_URL" | grep -q 'files/'; then + FILE_ID_ENC=$(echo "$APP_URL" | sed 's/.*files\///') + else + FILE_ID_ENC=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote('${FILE_ID}', safe=''))") + fi + WOPI_SRC="http://${BRIDGE_IP}:9300/wopi/files/${FILE_ID_ENC}" + echo "WOPI_SRC=${WOPI_SRC}" + + FAILED=() + SHARED_GROUPS=(BaseWopiViewing CheckFileInfoSchema EditFlows Locks AccessTokens GetLock ExtendedLockLength FileVersion Features) + + for group in "${SHARED_GROUPS[@]}"; do + echo "Running testgroup [${group}]" + docker run --rm --workdir /app \ + --entrypoint /app/Microsoft.Office.WopiValidator \ + "$VALIDATOR_IMAGE" \ + -t "$ACCESS_TOKEN" -w "$WOPI_SRC" -l "$TTL" --testgroup "$group" \ + || FAILED+=("$group") + done + + if [[ ${#FAILED[@]} -gt 0 ]]; then + echo "Failed testgroups: ${FAILED[*]}" + exit 1 + fi + echo "All WOPI validator tests passed." + - name: Stop oCIS and cs3 server + if: always() + run: | + if [[ -f /tmp/ocis-direct.pid ]]; then kill "$(cat /tmp/ocis-direct.pid)" 2>/dev/null || true; fi + docker rm -f wopi-fakeoffice wopi-cs3server 2>/dev/null || true all-acceptance-tests: - needs: [local-api-tests, cli-tests, core-api-tests, litmus, cs3api, wopi-builtin, wopi-cs3, e2e-tests] + needs: [api-tests-plain, api-tests-email, api-tests-tika, api-tests-antivirus, api-tests-auth-app, api-tests-ocm, api-tests-wopi, cli-tests, core-api-tests, litmus, cs3api, wopi-builtin, wopi-cs3, e2e-tests] runs-on: ubuntu-latest if: always() steps: diff --git a/.github/workflows/composite-actions-test.yml b/.github/workflows/composite-actions-test.yml new file mode 100644 index 00000000000..9f850da6cbc --- /dev/null +++ b/.github/workflows/composite-actions-test.yml @@ -0,0 +1,49 @@ +name: Composite Actions Test + +on: + workflow_dispatch: + +jobs: + composite-actions-apiGraph: + name: composite-actions-apiGraph + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + cache: true + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: "24" + - name: Enable pnpm + run: corepack enable && corepack prepare pnpm@10.28.1 --activate + - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 + with: + php-version: "8.4" + extensions: curl, xml, mbstring, zip, ldap, gd + tools: composer + + - name: Build oCIS from source + run: | + make ci-node-generate + make -C ocis build + + - name: Setup oCIS + uses: ./.github/actions/ocis-setup + with: + ocis-binary: ocis/bin/ocis + + - name: Test apiGraph + uses: ./.github/actions/ocis-test + with: + suite: apiGraph + + - name: Stop oCIS + if: always() + shell: bash + run: | + if [[ -f /tmp/ocis-wrapper.pid ]]; then + kill "$(cat /tmp/ocis-wrapper.pid)" 2>/dev/null || true + rm -f /tmp/ocis-wrapper.pid /tmp/ocis-server.log + fi diff --git a/.github/workflows/k6-load-test.yml b/.github/workflows/k6-load-test.yml index 93f3a215c1d..0a0e1057a3a 100644 --- a/.github/workflows/k6-load-test.yml +++ b/.github/workflows/k6-load-test.yml @@ -42,11 +42,11 @@ jobs: - name: Run k6 load tests if: ${{ !cancelled() }} - run: sh tests/config/drone/run_k6_tests.sh + run: sh tests/config/ci/run_k6_tests.sh - name: Retrieve OCIS logs if: ${{ !cancelled() }} - run: sh tests/config/drone/run_k6_tests.sh --ocis-log + run: sh tests/config/ci/run_k6_tests.sh --ocis-log - name: Show Grafana dashboard link if: ${{ !cancelled() }} diff --git a/tests/acceptance/run-cs3api.py b/tests/acceptance/run-cs3api.py deleted file mode 100644 index 0d9de34cb72..00000000000 --- a/tests/acceptance/run-cs3api.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -""" -Run CS3 API validator tests locally and in GitHub Actions CI. - -Usage: python3 tests/acceptance/run-cs3api.py -""" - -import os -import shutil -import signal -import subprocess -import sys -import time -from pathlib import Path - -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- - -# HTTPS — matching drone: ocis init generates a self-signed cert; proxy uses TLS by default. -# Host-side curl calls use -k (insecure) to skip cert verification. -OCIS_URL = "https://127.0.0.1:9200" -CS3API_IMAGE = "owncloud/cs3api-validator:0.2.1" - - -def get_docker_bridge_ip() -> str: - """Return the Docker bridge gateway IP, reachable from host and Docker containers.""" - r = subprocess.run( - ["docker", "network", "inspect", "bridge", - "--format", "{{range .IPAM.Config}}{{.Gateway}}{{end}}"], - capture_output=True, text=True, check=True, - ) - return r.stdout.strip() - - -def base_server_env(repo_root: Path, ocis_config_dir: str, ocis_public_url: str) -> dict: - """OCIS server environment matching drone ocisServer(deploy_type='cs3api_validator').""" - return { - "OCIS_URL": ocis_public_url, - "OCIS_CONFIG_DIR": ocis_config_dir, - "STORAGE_USERS_DRIVER": "ocis", - "PROXY_ENABLE_BASIC_AUTH": "true", - # No PROXY_TLS override — drone lets ocis use its default TLS (self-signed cert from init) - # IDP excluded: its static assets are absent when running as a host process - "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", - "WEB_UI_CONFIG_FILE": str(repo_root / "tests/config/ci/ocis-config.json"), - # cs3api_validator extras (drone ocisServer deploy_type="cs3api_validator") - "GATEWAY_GRPC_ADDR": "0.0.0.0:9142", - "OCIS_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD": "false", - } - - -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 main() -> int: - repo_root = Path(__file__).resolve().parents[2] - ocis_bin = repo_root / "ocis/bin/ocis" - ocis_config_dir = Path.home() / ".ocis/config" - - subprocess.run(["make", "-C", str(repo_root / "ocis"), "build"], check=True) - - # Docker bridge gateway IP: reachable from both the host and Docker containers. - # cs3api-validator connects to the GRPC gateway at {bridge_ip}:9142. - bridge_ip = get_docker_bridge_ip() - print(f"Docker bridge IP: {bridge_ip}", flush=True) - - server_env = {**os.environ} - server_env.update(base_server_env(repo_root, str(ocis_config_dir), - f"https://{bridge_ip}:9200")) - - subprocess.run( - [str(ocis_bin), "init", "--insecure", "true"], - env=server_env, - check=True, - ) - shutil.copy( - repo_root / "tests/config/ci/app-registry.yaml", - ocis_config_dir / "app-registry.yaml", - ) - - print("Starting ocis...", flush=True) - ocis_proc = subprocess.Popen( - [str(ocis_bin), "server"], - env=server_env, - ) - - def cleanup(*_): - try: - ocis_proc.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.", flush=True) - - print(f"\nRunning cs3api-validator against {bridge_ip}:9142", flush=True) - result = subprocess.run( - ["docker", "run", "--rm", - "--entrypoint", "/usr/bin/cs3api-validator", - CS3API_IMAGE, - "/var/lib/cs3api-validator", - f"--endpoint={bridge_ip}:9142"], - ) - return result.returncode - - finally: - cleanup() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/acceptance/run-e2e.py b/tests/acceptance/run-e2e.py deleted file mode 100755 index 06d46e271f7..00000000000 --- a/tests/acceptance/run-e2e.py +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env python3 -""" -Run Playwright e2e tests against a local OCIS instance. - -Usage: E2E_ARGS='--run-part 1' python3 tests/acceptance/run-e2e.py -Optional: TIKA_NEEDED=true, KEYCLOAK_NEEDED=true -""" - -import json -import os -import sys -import shlex -import subprocess -import signal -import time -import shutil -from pathlib import Path - -WEB_REPO = "https://github.com/owncloud/web.git" - - -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, use_basic_auth: bool = True) -> bool: - if use_basic_auth: - cmd = ["curl", "-sk", "-uadmin:admin", - f"{ocis_url}/graph/v1.0/users/admin", - "-w", "%{http_code}", "-o", "/dev/null"] - else: - # Keycloak mode: no local admin user, check unauthenticated endpoint - cmd = ["curl", "-sk", - f"{ocis_url}/.well-known/openid-configuration", - "-w", "%{http_code}", "-o", "/dev/null"] - r = subprocess.run(cmd, capture_output=True, text=True) - return r.stdout.strip() == "200" - - -def run(cmd: list, env: dict = None, check: bool = True, cwd=None): - e = {**os.environ, **(env or {})} - return subprocess.run(cmd, env=e, check=check, cwd=cwd) - - -def main() -> int: - e2e_args = os.environ.get("E2E_ARGS", "").strip() - if not e2e_args: - print("E2E_ARGS is required, e.g. E2E_ARGS='--run-part 1' python3 run-e2e.py", - file=sys.stderr) - return 1 - - tika_needed = os.environ.get("TIKA_NEEDED", "").lower() == "true" - keycloak_needed = os.environ.get("KEYCLOAK_NEEDED", "").lower() == "true" - - 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://127.0.0.1:9200" - ocis_config_dir = Path.home() / ".ocis/config" - web_dir = repo_root / "webTestRunner" - - # build ocis + ociswrapper only if not already provided (e.g. via artifact) - if not ocis_bin.exists(): - run(["make", "-C", str(repo_root / "ocis"), "build"]) - if not wrapper_bin.exists(): - run(["make", "-C", str(repo_root / "tests/ociswrapper"), "build"], - env={"GOWORK": "off"}) - - # clone + install web only if not already provided (e.g. via artifact) - if not web_dir.exists(): - ci_env = {} - ci_env_file = repo_root / "ci.env" - if ci_env_file.exists(): - for line in ci_env_file.read_text().splitlines(): - if "=" in line and not line.startswith("#"): - k, v = line.split("=", 1) - ci_env[k.strip()] = v.strip() - web_branch = ci_env.get("WEB_BRANCH", "master") - web_commitid = ci_env.get("WEB_COMMITID", "") - run(["git", "clone", "-b", web_branch, "--single-branch", "--no-tags", - WEB_REPO, str(web_dir)]) - if web_commitid: - subprocess.run(["git", "checkout", web_commitid], cwd=web_dir, check=True) - - if not (web_dir / "node_modules").exists(): - pkg_manager = json.loads((web_dir / "package.json").read_text()).get("packageManager", "pnpm") - run(["npm", "install", "--silent", "--global", "--force", pkg_manager]) - subprocess.run(["pnpm", "config", "set", "store-dir", "./.pnpm-store"], - cwd=web_dir, check=True) - subprocess.run(["pnpm", "install"], cwd=web_dir, check=True) - subprocess.run(["pnpm", "exec", "playwright", "install", "--with-deps", "chromium"], - cwd=web_dir, check=True) - - # init ocis - run([str(ocis_bin), "init", "--insecure", "true"]) - shutil.copy( - repo_root / "tests/config/ci/app-registry.yaml", - ocis_config_dir / "app-registry.yaml", - ) - - # Trust the self-signed cert system-wide so Chromium and node-fetch - # don't produce TLS handshake errors that load the server - ocis_cert = Path.home() / ".ocis/proxy/server.crt" - if ocis_cert.exists(): - subprocess.run( - ["sudo", "cp", str(ocis_cert), "/usr/local/share/ca-certificates/ocis.crt"], - check=True, - ) - subprocess.run(["sudo", "update-ca-certificates"], check=True) - - # Patch web UI config: replace Drone Docker service name with our URL - drone_web_cfg = json.loads( - (repo_root / "tests/config/ci/ocis-config.json").read_text() - ) - - def _patch_urls(obj, old, new): - if isinstance(obj, dict): - return {k: _patch_urls(v, old, new) for k, v in obj.items()} - if isinstance(obj, list): - return [_patch_urls(v, old, new) for v in obj] - if isinstance(obj, str): - return obj.replace(old, new) - return obj - - gha_web_cfg = _patch_urls(drone_web_cfg, "https://ocis-server:9200", ocis_url) - gha_web_cfg_path = ocis_config_dir / "web-ui-config.json" - gha_web_cfg_path.write_text(json.dumps(gha_web_cfg, indent=2)) - - server_env = { - **os.environ, - # core — matches drone ocisServer() - "OCIS_URL": ocis_url, - "OCIS_CONFIG_DIR": str(ocis_config_dir), - "STORAGE_USERS_DRIVER": "ocis", - "PROXY_ENABLE_BASIC_AUTH": "true", - - "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(gha_web_cfg_path), - "THUMBNAILS_TXT_FONTMAP_FILE": str(repo_root / "tests/config/ci/fontsMap.json"), - # extra_server_environment — matches drone e2eTestPipeline() - "OCIS_PASSWORD_POLICY_BANNED_PASSWORDS_LIST": str(repo_root / "tests/config/ci/banned-password-list.txt"), - "GRAPH_AVAILABLE_ROLES": "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5,a8d5fe5e-96e3-418d-825b-534dbdf22b99,fb6c3e19-e378-47e5-b277-9732f9de6e21,58c63c02-1d89-4572-916a-870abc5a1b7d,2d00ce52-1fc2-4dbc-8b95-a73b73395f5a,1c996275-f1c9-4e71-abdf-a42f6495e960,312c0871-5ef7-4b3a-85b6-0e4074c64049,aa97fe03-7980-45ac-9e50-b325749fd7e6,63e64e19-8d43-42ec-a738-2b6af2610efa", - "FRONTEND_CONFIGURABLE_NOTIFICATIONS": "true", - # 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", - } - - if tika_needed: - server_env.update({ - "FRONTEND_FULL_TEXT_SEARCH_ENABLED": "true", - "SEARCH_EXTRACTOR_TYPE": "tika", - "SEARCH_EXTRACTOR_TIKA_TIKA_URL": "http://localhost:9998", - "SEARCH_EXTRACTOR_CS3SOURCE_INSECURE": "true", - }) - - if keycloak_needed: - server_env.update({ - "OCIS_EXCLUDE_RUN_SERVICES": "idp", - "PROXY_AUTOPROVISION_ACCOUNTS": "true", - "PROXY_ROLE_ASSIGNMENT_DRIVER": "oidc", - "OCIS_OIDC_ISSUER": "https://localhost:8443/realms/oCIS", - "PROXY_OIDC_REWRITE_WELLKNOWN": "true", - "WEB_OIDC_CLIENT_ID": "web", - "PROXY_USER_OIDC_CLAIM": "preferred_username", - "PROXY_USER_CS3_CLAIM": "username", - "OCIS_ADMIN_USER_ID": "", - "GRAPH_ASSIGN_DEFAULT_USER_ROLE": "false", - "GRAPH_USERNAME_MATCH": "none", - "PROXY_CSP_CONFIG_FILE_LOCATION": str(repo_root / "tests/config/ci/csp.yaml"), - "KEYCLOAK_DOMAIN": "localhost:8443", - "IDM_CREATE_DEMO_USERS": "false", - }) - - procs = [] - - print("Starting ocis...") - if keycloak_needed: - # external IdP: run ocis server directly (no ociswrapper) - ocis_proc = subprocess.Popen( - [str(ocis_bin), "server"], - env=server_env, - ) - else: - ocis_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(ocis_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, use_basic_auth=not keycloak_needed), 300, "ocis") - print("ocis ready.") - - playwright_env = { - **os.environ, - "BASE_URL_OCIS": ocis_url, - "HEADLESS": "true", - "RETRY": "3", - "SKIP_A11Y_TESTS": "true", - "REPORT_TRACING": "true", - "NODE_EXTRA_CA_CERTS": str(ocis_cert), - "BROWSER": "chromium", - } - - if keycloak_needed: - playwright_env.update({ - "KEYCLOAK": "true", - "KEYCLOAK_HOST": "localhost:8443", - }) - - print(f"Running e2e: {e2e_args}") - result = subprocess.run( - ["bash", "run-e2e.sh"] + shlex.split(e2e_args), - cwd=web_dir / "tests/e2e", - env=playwright_env, - ) - return result.returncode - - finally: - cleanup() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/acceptance/run-github.py b/tests/acceptance/run-github.py deleted file mode 100755 index 4e11fd085ee..00000000000 --- a/tests/acceptance/run-github.py +++ /dev/null @@ -1,722 +0,0 @@ -#!/usr/bin/env python3 -""" -Run ocis acceptance tests locally and in GitHub Actions CI. - -Usage: BEHAT_SUITES=apiGraph python3 tests/acceptance/run-github.py -""" - -import json -import os -import sys -import subprocess -import signal -import time -import tempfile -import shutil -from pathlib import Path - -# --------------------------------------------------------------------------- - -# 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", - "OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE": "", # set at runtime - "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", - }, - }, - "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 - - -# GitHub Actions uses --network host: all wopi services share one network namespace. -# Drone gives each service its own container → all can use 9300/9301/9304. -# Assign distinct ports here to avoid collisions. -_WOPI_PORTS = { - "collabora": {"grpc": 9301, "http": 9300, "debug": 9304}, - "onlyoffice": {"grpc": 9311, "http": 9310, "debug": 9314}, - "fakeoffice": {"grpc": 9321, "http": 9320, "debug": 9324}, -} - - -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_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", - "MICRO_REGISTRY_ADDRESS": "127.0.0.1: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/ci/ocis-config.json"), - "THUMBNAILS_TXT_FONTMAP_FILE": str(repo_root / "tests/config/ci/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, container: str = None) -> None: - start = time.time() - deadline = start + timeout - last_log = start - while not condition_fn(): - now = time.time() - if now > deadline: - elapsed = int(now - start) - print(f"Timeout waiting for {label} after {elapsed}s", file=sys.stderr) - # dump docker diagnostics — use explicit container name if provided - cname = container or label - for cmd in ( - ["docker", "ps", "-a", "--filter", f"name={cname}", "--no-trunc"], - ["docker", "logs", "--tail", "50", cname], - ): - r = subprocess.run(cmd, capture_output=True, text=True) - if r.stdout.strip(): - print(f"--- {' '.join(cmd)} ---", file=sys.stderr) - print(r.stdout, file=sys.stderr) - if r.stderr.strip(): - print(r.stderr, file=sys.stderr) - sys.exit(1) - if now - last_log >= 30: - print(f" Waiting for {label}... {int(now - start)}s") - last_log = now - 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 _tcp_ready(host: str, port: int) -> bool: - """Check if a TCP port is accepting connections.""" - import socket - try: - with socket.create_connection((host, port), timeout=2): - return True - except (ConnectionRefusedError, OSError): - return False - - -def clamav_healthy() -> bool: - return _tcp_ready("localhost", 3310) - - - -def load_env_file(path: Path) -> dict: - """Parse a bash-style env file (export KEY=value) into a dict.""" - env = {} - for line in path.read_text().splitlines(): - line = line.strip() - if not line or line.startswith("#") or line.startswith("!"): - continue - line = line.removeprefix("export ").strip() - if "=" in line: - k, v = line.split("=", 1) - env[k.strip()] = v.strip() - return env - - -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" - - ocis_fed_url = "https://localhost:10200" - - cfg = merged_config(suites) - print(f"Suites: {suites}") - print(f"Services: email={cfg['emailNeeded']} tika={cfg['tikaNeeded']} " - f"antivirus={cfg['antivirusNeeded']} federation={cfg['federationServer']} " - f"wopi={cfg['collaborationServiceNeeded']}") - - # generate IDP web assets (required for IDP service to start; matches drone ci-node-generate) - run(["make", "-C", str(repo_root / "services/idp"), "ci-node-generate"]) - # download web UI assets (required for robots.txt and other static assets; no pnpm needed) - run(["make", "-C", str(repo_root / "services/web"), "ci-node-generate"]) - - # build (ENABLE_VIPS=true when libvips-dev is installed, matching drone) - build_env = {} - if subprocess.run(["pkg-config", "--exists", "vips"], - capture_output=True).returncode == 0: - build_env["ENABLE_VIPS"] = "true" - run(["make", "-C", str(repo_root / "ocis"), "build"], env=build_env) - 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.") - - # OCM federation: rewrite providers.json with localhost URLs - if cfg["federationServer"]: - providers_src = repo_root / "tests/config/ci/providers.json" - providers = json.loads(providers_src.read_text()) - for p in providers: - # replace container DNS names with localhost - p["domain"] = p["domain"].replace("ocis-server:9200", "localhost:9200") - p["domain"] = p["domain"].replace("federation-ocis-server:10200", "localhost:10200") - for svc in p.get("services", []): - ep = svc.get("endpoint", {}) - ep["path"] = ep.get("path", "").replace("ocis-server:9200", "localhost:9200") - ep["path"] = ep.get("path", "").replace("federation-ocis-server:10200", "localhost:10200") - svc["host"] = svc.get("host", "").replace("ocis-server:9200", "localhost:9200") - svc["host"] = svc.get("host", "").replace("federation-ocis-server:10200", "localhost:10200") - providers_tmp = tempfile.NamedTemporaryFile( - mode="w", suffix=".json", prefix="ocm-providers-", delete=False) - json.dump(providers, providers_tmp) - providers_tmp.close() - cfg["extraServerEnvironment"]["OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE"] = providers_tmp.name - - # init ocis - run([str(ocis_bin), "init", "--insecure", "true"]) - shutil.copy( - repo_root / "tests/config/ci/app-registry.yaml", - ocis_config_dir / "app-registry.yaml", - ) - - # generate fontsMap.json with the correct absolute font path for this runner - font_path = str(repo_root / "tests/config/ci/NotoSans.ttf") - fontmap_tmp = tempfile.NamedTemporaryFile( - mode="w", suffix=".json", prefix="fontsMap-", delete=False) - json.dump({"defaultFont": font_path}, fontmap_tmp) - fontmap_tmp.close() - - # assemble ocis server env - server_env = {**os.environ} - server_env.update(base_server_env(repo_root, ocis_url, str(ocis_config_dir))) - server_env["THUMBNAILS_TXT_FONTMAP_FILE"] = fontmap_tmp.name - server_env.update(cfg["extraServerEnvironment"]) - - # start ociswrapper (primary ocis) - 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) - - # start federation ocis server (second instance on port 10200) - if cfg["federationServer"]: - fed_config_dir = Path.home() / ".ocis-federation/config" - fed_config_dir.mkdir(parents=True, exist_ok=True) - fed_data_dir = Path.home() / ".ocis-federation" - - fed_env = {**os.environ} - fed_env.update(base_server_env(repo_root, ocis_fed_url, str(fed_config_dir))) - fed_env.update(cfg["extraServerEnvironment"]) - # load federation port mappings from canonical env file (single source of truth) - fed_env.update(load_env_file(repo_root / "tests/config/local/.env-federation")) - # CI-specific overrides - fed_env.update({ - "OCIS_URL": ocis_fed_url, - "OCIS_BASE_DATA_PATH": str(fed_data_dir), - "OCIS_CONFIG_DIR": str(fed_config_dir), - "OCIS_RUNTIME_PORT": "10250", - "MICRO_REGISTRY_ADDRESS": "127.0.0.1:10233", - }) - - # init federation ocis with separate config - run([str(ocis_bin), "init", "--insecure", "true", - "--config-path", str(fed_config_dir)]) - shutil.copy( - repo_root / "tests/config/ci/app-registry.yaml", - fed_config_dir / "app-registry.yaml", - ) - - print("Starting federation ocis...") - fed_proc = subprocess.Popen( - [str(ocis_bin), "server"], - env=fed_env, - ) - procs.append(fed_proc) - - # --------------------------------------------------------------------------- - # Collaboration service helpers — same names/call pattern as drone.star - # Only deviation: container hostnames → localhost - # --------------------------------------------------------------------------- - def fakeOffice(): - # BusyBox nc -k has a race between connections: after each response the - # while-loop restarts nc, leaving a window where connections are refused. - # The collaboration service hits that window at startup → readLoopPeekFailLocked - # → healthz never binds → 300s timeout. Use Python's built-in HTTP server - # instead; it handles concurrent connections without gaps. - run(["docker", "run", "-d", "--name", "fakeoffice", "--network", "host", - "-v", f"{repo_root}:/ocis:ro", - "python:3-alpine", - "python3", "-c", - "import http.server, pathlib\n" - "body = pathlib.Path('/ocis/tests/config/ci/hosting-discovery.xml').read_bytes()\n" - "class H(http.server.BaseHTTPRequestHandler):\n" - " def do_GET(self):\n" - " self.send_response(200)\n" - " self.send_header('Content-Type', 'text/xml')\n" - " self.end_headers()\n" - " self.wfile.write(body)\n" - " def log_message(self, *a): pass\n" - "http.server.HTTPServer(('', 8080), H).serve_forever()\n" - ]) - return [] - - def collaboraService(): - # drone commands copy-pasted verbatim - run(["docker", "run", "-d", "--name", "collabora", "--network", "host", - "-e", "DONT_GEN_SSL_CERT=set", - "-e", f"extra_params=--o:ssl.enable=true --o:ssl.termination=true " - f"--o:welcome.enable=false --o:net.frame_ancestors=https://localhost:9200", - "--entrypoint", "/bin/sh", - "collabora/code:24.04.5.1.1", - "-c", "\n".join([ - "set -e", - "coolconfig generate-proof-key", - "bash /start-collabora-online.sh", - ])]) - return [] - - def onlyofficeService(): - # GitHub runner ships PostgreSQL pre-started on 5432. - # OnlyOffice supervisord starts its own PostgreSQL on 5432 internally. - # With --network host both compete for the same port → OnlyOffice DB never - # starts → docservice stays down → nginx returns 502 forever. - # Drone avoids this because each service has its own network namespace. - subprocess.run(["sudo", "systemctl", "stop", "postgresql"], - capture_output=True) - only_office_json = repo_root / "tests/config/ci/only-office.json" - run(["docker", "run", "-d", "--name", "onlyoffice", "--network", "host", - "-e", "WOPI_ENABLED=true", - "-e", "USE_UNAUTHORIZED_STORAGE=true", - "-v", f"{only_office_json}:/tmp/only-office.json:ro", - "--entrypoint", "/bin/sh", - "onlyoffice/documentserver:9.0.0", - "-c", "\n".join([ - "set -e", - "cp /tmp/only-office.json /etc/onlyoffice/documentserver/local.json", - "openssl req -x509 -newkey rsa:4096 -keyout onlyoffice.key -out onlyoffice.crt -sha256 -days 365 -batch -nodes", - "mkdir -p /var/www/onlyoffice/Data/certs", - "cp onlyoffice.key /var/www/onlyoffice/Data/certs/", - "cp onlyoffice.crt /var/www/onlyoffice/Data/certs/", - "chmod 400 /var/www/onlyoffice/Data/certs/onlyoffice.key", - "/app/ds/run-document-server.sh", - ])]) - return [] - - def wopiCollaborationService(name, ocis_url=ocis_url): - # drone: startOcisService("collaboration", "wopi-{name}", environment) - # runs: ocis/bin/ocis-debug collaboration server - service_name = "wopi-%s" % name - ports = _WOPI_PORTS[name] - environment = { - **os.environ, - "OCIS_URL": ocis_url, - "MICRO_REGISTRY": "nats-js-kv", - "MICRO_REGISTRY_ADDRESS": "localhost:9233", - "COLLABORATION_LOG_LEVEL": "debug", - "COLLABORATION_GRPC_ADDR": f"0.0.0.0:{ports['grpc']}", - "COLLABORATION_HTTP_ADDR": f"0.0.0.0:{ports['http']}", - "COLLABORATION_DEBUG_ADDR": f"0.0.0.0:{ports['debug']}", - "COLLABORATION_APP_PROOF_DISABLE": "true", - "COLLABORATION_APP_INSECURE": "true", - "COLLABORATION_CS3API_DATAGATEWAY_INSECURE": "true", - "OCIS_JWT_SECRET": "some-ocis-jwt-secret", - "COLLABORATION_WOPI_SECRET": "some-wopi-secret", - } - if name == "collabora": - environment["COLLABORATION_APP_NAME"] = "Collabora" - environment["COLLABORATION_APP_PRODUCT"] = "Collabora" - environment["COLLABORATION_APP_ADDR"] = "https://localhost:9980" - environment["COLLABORATION_APP_ICON"] = "https://localhost:9980/favicon.ico" - elif name == "onlyoffice": - environment["COLLABORATION_APP_NAME"] = "OnlyOffice" - environment["COLLABORATION_APP_PRODUCT"] = "OnlyOffice" - environment["COLLABORATION_APP_ADDR"] = "https://localhost:443" - environment["COLLABORATION_APP_ICON"] = "https://localhost:443/web-apps/apps/documenteditor/main/resources/img/favicon.ico" - elif name == "fakeoffice": - environment["COLLABORATION_APP_NAME"] = "FakeOffice" - environment["COLLABORATION_APP_PRODUCT"] = "Microsoft" - environment["COLLABORATION_APP_ADDR"] = "http://localhost:8080" - environment["COLLABORATION_WOPI_SRC"] = f"http://localhost:{ports['http']}" - print(f"Starting {service_name}...") - return [subprocess.Popen([str(ocis_bin), "collaboration", "server"], env=environment)] - - def ocisHealthCheck(name, services=[]): - # drone: curl healthz + readyz on each service (timeout 300s) - for service in services: - host, port = service.rsplit(":", 1) - for endpoint in ("healthz", "readyz"): - wait_for( - lambda h="localhost", p=int(port), ep=endpoint: subprocess.run( - ["curl", "-sf", f"http://{h}:{p}/{ep}"], capture_output=True - ).returncode == 0, - 300, f"{service}/{endpoint}", - ) - print(f"health-check-{name}: all services healthy.") - - def wopi_discovery_ready(app_url: str) -> bool: - """Return True once the WOPI app's /hosting/discovery returns HTTP 200.""" - url = app_url.rstrip("/") + "/hosting/discovery" - r = subprocess.run( - ["curl", "-sfk", url], capture_output=True - ) - return r.returncode == 0 - - # drone.star non-k8s collaborationServiceNeeded path (lines 1195-1196, 1140-1141, 1179-1183) - if cfg["collaborationServiceNeeded"]: - procs += fakeOffice() + collaboraService() + onlyofficeService() - # Wait for each app's /hosting/discovery to return 200 before starting its - # collaboration service. GetAppURLs in server.go calls discovery synchronously - # at startup — non-200 exits the process immediately, healthz never binds. - wait_for(lambda: wopi_discovery_ready("http://localhost:8080"), 300, "fakeoffice discovery", container="fakeoffice") - wait_for(lambda: wopi_discovery_ready("https://localhost:9980"), 300, "collabora discovery", container="collabora") - wait_for(lambda: wopi_discovery_ready("https://localhost:443"), 300, "onlyoffice discovery", container="onlyoffice") - procs += wopiCollaborationService("fakeoffice") + \ - wopiCollaborationService("collabora") + \ - wopiCollaborationService("onlyoffice") - ocisHealthCheck("wopi", [ - f"localhost:{_WOPI_PORTS['collabora']['debug']}", - f"localhost:{_WOPI_PORTS['onlyoffice']['debug']}", - f"localhost:{_WOPI_PORTS['fakeoffice']['debug']}", - ]) - - 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.") - - if cfg["federationServer"]: - wait_for(lambda: ocis_healthy(ocis_fed_url), 300, "federation ocis") - print("federation 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 only when not using remote.php - # (mirrors drone.star: "" if run_with_remote_php else "cat ...without-remotephp.md >> ...") - tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) - tmp.write(base_failures.read_text()) - if os.environ.get("WITH_REMOTE_PHP", "false").lower() != "true": - 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, - "TEST_SERVER_FED_URL": ocis_fed_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, - "COLLABORATION_SERVICE_URL": f"http://localhost:{_WOPI_PORTS['fakeoffice']['http']}", - } - 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-litmus.py b/tests/acceptance/run-litmus.py deleted file mode 100644 index 7b775cbf3a8..00000000000 --- a/tests/acceptance/run-litmus.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python3 -""" -Run litmus WebDAV compliance tests locally and in GitHub Actions CI. - -Usage: python3 tests/acceptance/run-litmus.py -""" - -import json -import os -import re -import shutil -import signal -import subprocess -import sys -import time -from pathlib import Path - -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- - -# HTTPS — matching drone: ocis init generates a self-signed cert; proxy uses TLS by default. -# Host-side curl calls use -k (insecure) to skip cert verification. -OCIS_URL = "https://127.0.0.1:9200" -LITMUS_IMAGE = "owncloudci/litmus:latest" -LITMUS_TESTS = "basic copymove props http" -SHARE_ENDPOINT = "ocs/v2.php/apps/files_sharing/api/v1/shares" - - -def get_docker_bridge_ip() -> str: - """Return the Docker bridge gateway IP, reachable from host and Docker containers.""" - r = subprocess.run( - ["docker", "network", "inspect", "bridge", - "--format", "{{range .IPAM.Config}}{{.Gateway}}{{end}}"], - capture_output=True, text=True, check=True, - ) - return r.stdout.strip() - - -def base_server_env(repo_root: Path, ocis_config_dir: str, ocis_public_url: str) -> dict: - """OCIS server environment matching drone ocisServer() for litmus.""" - return { - "OCIS_URL": ocis_public_url, - "OCIS_CONFIG_DIR": ocis_config_dir, - "STORAGE_USERS_DRIVER": "ocis", - "PROXY_ENABLE_BASIC_AUTH": "true", - # No PROXY_TLS override — drone lets ocis use its default TLS (self-signed cert from init) - # IDP excluded: its static assets are absent when running as a host process - "OCIS_EXCLUDE_RUN_SERVICES": "idp", - "OCIS_LOG_LEVEL": "error", - "IDM_CREATE_DEMO_USERS": "true", - "IDM_ADMIN_PASSWORD": "admin", - "FRONTEND_SEARCH_MIN_LENGTH": "2", - "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", - "WEB_UI_CONFIG_FILE": str(repo_root / "tests/config/ci/ocis-config.json"), - } - - -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 setup_for_litmus(ocis_url: str) -> tuple: - """ - Translate tests/config/ci/setup-for-litmus.sh to Python. - Returns (space_id, public_token). - """ - # get personal space ID - r = subprocess.run( - ["curl", "-sk", "-uadmin:admin", f"{ocis_url}/graph/v1.0/me/drives"], - capture_output=True, text=True, check=True, - ) - drives = json.loads(r.stdout) - space_id = "" - for drive in drives.get("value", []): - if drive.get("driveType") == "personal": - web_dav_url = drive.get("root", {}).get("webDavUrl", "") - # last non-empty path segment (same as cut -d"/" -f6 in bash) - space_id = [p for p in web_dav_url.split("/") if p][-1] - break - if not space_id: - print("ERROR: could not determine personal space ID", file=sys.stderr) - sys.exit(1) - print(f"SPACE_ID={space_id}") - - # create test folder as einstein - subprocess.run( - ["curl", "-sk", "-ueinstein:relativity", "-X", "MKCOL", - f"{ocis_url}/remote.php/webdav/new_folder"], - capture_output=True, check=True, - ) - - # create share from einstein to admin - r = subprocess.run( - ["curl", "-sk", "-ueinstein:relativity", - f"{ocis_url}/{SHARE_ENDPOINT}", - "-d", "path=/new_folder&shareType=0&permissions=15&name=new_folder&shareWith=admin"], - capture_output=True, text=True, check=True, - ) - share_id_match = re.search(r"(.+?)", r.stdout) - if share_id_match: - share_id = share_id_match.group(1) - # accept the share as admin - subprocess.run( - ["curl", "-X", "POST", "-sk", "-uadmin:admin", - f"{ocis_url}/{SHARE_ENDPOINT}/pending/{share_id}"], - capture_output=True, check=True, - ) - - # create public share as einstein - r = subprocess.run( - ["curl", "-sk", "-ueinstein:relativity", - f"{ocis_url}/{SHARE_ENDPOINT}", - "-d", "path=/new_folder&shareType=3&permissions=15&name=new_folder"], - capture_output=True, text=True, check=True, - ) - public_token = "" - token_match = re.search(r"(.+?)", r.stdout) - if token_match: - public_token = token_match.group(1) - print(f"PUBLIC_TOKEN={public_token}") - - return space_id, public_token - - -def run_litmus(name: str, endpoint: str) -> int: - print(f"\nTesting endpoint [{name}]: {endpoint}", flush=True) - result = subprocess.run( - ["docker", "run", "--rm", - "-e", f"LITMUS_URL={endpoint}", - "-e", "LITMUS_USERNAME=admin", - "-e", "LITMUS_PASSWORD=admin", - "-e", f"TESTS={LITMUS_TESTS}", - LITMUS_IMAGE, - # No extra CMD — ENTRYPOINT is already litmus-wrapper; passing it again - # would make the wrapper use the path as LITMUS_URL, overriding the env var. - ], - ) - return result.returncode - - -def main() -> int: - repo_root = Path(__file__).resolve().parents[2] - ocis_bin = repo_root / "ocis/bin/ocis" - ocis_config_dir = Path.home() / ".ocis/config" - - # build (matching drone: restores binary from cache, then runs ocis server directly) - subprocess.run(["make", "-C", str(repo_root / "ocis"), "build"], check=True) - - # Docker bridge gateway IP: reachable from both the host (via docker0 interface) - # and Docker containers (via bridge network default gateway). Use this as OCIS_URL - # so that any redirects OCIS generates stay on a hostname the litmus container - # can follow — matching how drone uses "ocis-server:9200" consistently. - # HTTPS: owncloudci/litmus accepts insecure (self-signed) certs, just like drone does. - bridge_ip = get_docker_bridge_ip() - litmus_base = f"https://{bridge_ip}:9200" - print(f"Docker bridge IP: {bridge_ip}", flush=True) - - # assemble server env first — same env vars drone sets on the container before - # running `ocis init`, so IDM_ADMIN_PASSWORD=admin is present during init and - # the config is written with the correct password (not a random one) - server_env = {**os.environ} - server_env.update(base_server_env(repo_root, str(ocis_config_dir), litmus_base)) - - # init ocis with full server env (mirrors drone: env is set before ocis init runs) - subprocess.run( - [str(ocis_bin), "init", "--insecure", "true"], - env=server_env, - check=True, - ) - shutil.copy( - repo_root / "tests/config/ci/app-registry.yaml", - ocis_config_dir / "app-registry.yaml", - ) - - # start ocis server directly (matching drone: no ociswrapper for litmus) - print("Starting ocis...", flush=True) - ocis_proc = subprocess.Popen( - [str(ocis_bin), "server"], - env=server_env, - ) - - def cleanup(*_): - try: - ocis_proc.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.", flush=True) - - space_id, _ = setup_for_litmus(OCIS_URL) - - endpoints = [ - ("old-endpoint", f"{litmus_base}/remote.php/webdav"), - ("new-endpoint", f"{litmus_base}/remote.php/dav/files/admin"), - ("new-shared", f"{litmus_base}/remote.php/dav/files/admin/Shares/new_folder/"), - ("old-shared", f"{litmus_base}/remote.php/webdav/Shares/new_folder/"), - ("spaces-endpoint", f"{litmus_base}/remote.php/dav/spaces/{space_id}"), - ] - - failed = [] - for name, endpoint in endpoints: - rc = run_litmus(name, endpoint) - if rc != 0: - failed.append(name) - - if failed: - print(f"\nFailed endpoints: {', '.join(failed)}", file=sys.stderr) - return 1 - print("\nAll litmus tests passed.") - return 0 - - finally: - cleanup() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/acceptance/run-wopi.py b/tests/acceptance/run-wopi.py deleted file mode 100644 index d152e72ae1f..00000000000 --- a/tests/acceptance/run-wopi.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/usr/bin/env python3 -""" -Run WOPI validator tests locally and in GitHub Actions CI. - -Usage: python3 tests/acceptance/run-wopi.py --type builtin - python3 tests/acceptance/run-wopi.py --type cs3 -""" - -import argparse -import json -import os -import re -import shutil -import signal -import socket -import subprocess -import sys -import time -import urllib.parse -from pathlib import Path - -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- - -# HTTPS — matching drone; host-side curl calls use -k. -OCIS_URL = "https://127.0.0.1:9200" -VALIDATOR_IMAGE = "owncloudci/wopi-validator" -CS3_WOPI_IMAGE = "cs3org/wopiserver:v10.4.0" -FAKEOFFICE_IMAGE = "owncloudci/alpine:latest" - -# Testgroups shared between both variants (drone: testgroups list) -SHARED_TESTGROUPS = [ - "BaseWopiViewing", - "CheckFileInfoSchema", - "EditFlows", - "Locks", - "AccessTokens", - "GetLock", - "ExtendedLockLength", - "FileVersion", - "Features", -] - -# Testgroups only run for builtin (drone: builtinOnlyTestGroups, with -s flag) -BUILTIN_ONLY_TESTGROUPS = [ - "PutRelativeFile", - "RenameFileIfCreateChildFileIsNotSupported", -] - - -def get_docker_bridge_ip() -> str: - r = subprocess.run( - ["docker", "network", "inspect", "bridge", - "--format", "{{range .IPAM.Config}}{{.Gateway}}{{end}}"], - capture_output=True, text=True, check=True, - ) - return r.stdout.strip() - - -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 tcp_reachable(host: str, port: int) -> bool: - try: - with socket.create_connection((host, port), timeout=1): - return True - except Exception: - return False - - -def wopi_discovery_ready(url: str) -> bool: - r = subprocess.run( - ["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", url], - capture_output=True, text=True, - ) - return r.stdout.strip() == "200" - - -def base_server_env(repo_root: Path, ocis_config_dir: str, ocis_public_url: str, - bridge_ip: str, wopi_type: str) -> dict: - """ - OCIS server environment matching drone ocisServer(deploy_type='wopi_validator'). - builtin: also excludes app-provider (collaboration service takes that role). - """ - exclude = "idp,app-provider" if wopi_type == "builtin" else "idp" - return { - "OCIS_URL": ocis_public_url, - "OCIS_CONFIG_DIR": ocis_config_dir, - "STORAGE_USERS_DRIVER": "ocis", - "PROXY_ENABLE_BASIC_AUTH": "true", - # No PROXY_TLS override — drone uses default TLS (self-signed cert from init) - # IDP excluded: static assets absent when running as host process - "OCIS_EXCLUDE_RUN_SERVICES": exclude, - "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", - "WEB_UI_CONFIG_FILE": str(repo_root / "tests/config/ci/ocis-config.json"), - # wopi_validator extras (drone ocisServer deploy_type="wopi_validator") - "GATEWAY_GRPC_ADDR": "0.0.0.0:9142", - "APP_PROVIDER_EXTERNAL_ADDR": "com.owncloud.api.app-provider", - "APP_PROVIDER_DRIVER": "wopi", - "APP_PROVIDER_WOPI_APP_NAME": "FakeOffice", - "APP_PROVIDER_WOPI_APP_URL": f"http://{bridge_ip}:8080", - "APP_PROVIDER_WOPI_INSECURE": "true", - "APP_PROVIDER_WOPI_WOPI_SERVER_EXTERNAL_URL": f"http://{bridge_ip}:9300", - "APP_PROVIDER_WOPI_FOLDER_URL_BASE_URL": ocis_public_url, - } - - -def collab_service_env(bridge_ip: str, ocis_config_dir: str) -> dict: - """ - Environment for 'ocis collaboration server' (builtin wopi-fakeoffice). - Mirrors drone wopiCollaborationService("fakeoffice"). - """ - return { - "OCIS_URL": f"https://{bridge_ip}:9200", - "OCIS_CONFIG_DIR": ocis_config_dir, - "MICRO_REGISTRY": "nats-js-kv", - "MICRO_REGISTRY_ADDRESS": "127.0.0.1:9233", - "COLLABORATION_LOG_LEVEL": "debug", - "COLLABORATION_GRPC_ADDR": "0.0.0.0:9301", - "COLLABORATION_HTTP_ADDR": "0.0.0.0:9300", - "COLLABORATION_DEBUG_ADDR": "0.0.0.0:9304", - "COLLABORATION_APP_PROOF_DISABLE": "true", - "COLLABORATION_APP_INSECURE": "true", - "COLLABORATION_CS3API_DATAGATEWAY_INSECURE": "true", - "OCIS_JWT_SECRET": "some-ocis-jwt-secret", - "COLLABORATION_WOPI_SECRET": "some-wopi-secret", - "COLLABORATION_APP_NAME": "FakeOffice", - "COLLABORATION_APP_PRODUCT": "Microsoft", - "COLLABORATION_APP_ADDR": f"http://{bridge_ip}:8080", - # COLLABORATION_WOPI_SRC is what OCIS tells clients to use — must be reachable - # from Docker validator containers (collaboration service runs as host process) - "COLLABORATION_WOPI_SRC": f"http://{bridge_ip}:9300", - } - - -def prepare_test_file(bridge_ip: str) -> tuple: - """ - Upload test.wopitest via WebDAV, open the WOPI app, extract credentials. - Mirrors the prepare-test-file step from drone.star. - Returns (access_token, access_token_ttl, wopi_src). - """ - headers_file = "/tmp/wopi-headers.txt" - - # PUT empty test file (--retry-connrefused/--retry-all-errors matching drone) - subprocess.run( - ["curl", "-sk", "-u", "admin:admin", "-X", "PUT", - "--fail", "--retry-connrefused", "--retry", "7", "--retry-all-errors", - f"{OCIS_URL}/remote.php/webdav/test.wopitest", - "-D", headers_file], - check=True, - ) - - # Extract Oc-Fileid from response headers - headers_text = Path(headers_file).read_text() - print("--- PUT headers ---", flush=True) - print(headers_text[:500], flush=True) - m = re.search(r"Oc-Fileid:\s*(\S+)", headers_text, re.IGNORECASE) - if not m: - print("ERROR: Oc-Fileid not found in PUT response headers", file=sys.stderr) - sys.exit(1) - file_id = m.group(1).strip() - print(f"FILE_ID={file_id}", flush=True) - - # POST to app/open to get WOPI access token and wopi src - url = f"{OCIS_URL}/app/open?app_name=FakeOffice&file_id={urllib.parse.quote(file_id, safe='')}" - r = subprocess.run( - ["curl", "-sk", "-u", "admin:admin", "-X", "POST", - "--fail", "--retry-connrefused", "--retry", "7", "--retry-all-errors", url], - capture_output=True, text=True, check=True, - ) - open_json = json.loads(r.stdout) - print(f"open.json: {r.stdout[:800]}", flush=True) - - access_token = open_json["form_parameters"]["access_token"] - access_token_ttl = str(open_json["form_parameters"]["access_token_ttl"]) - app_url = open_json.get("app_url", "") - - # Construct wopi_src: drone extracts file ID from app_url after 'files%2F', - # then prepends http://wopi-fakeoffice:9300/wopi/files/ — we use bridge_ip instead. - wopi_base = f"http://{bridge_ip}:9300/wopi/files/" - if "files%2F" in app_url: - file_id_encoded = app_url.split("files%2F")[-1].strip().strip('"') - elif "files/" in app_url: - file_id_encoded = app_url.split("files/")[-1].strip().strip('"') - else: - file_id_encoded = urllib.parse.quote(file_id, safe="") - wopi_src = wopi_base + file_id_encoded - print(f"WOPI_SRC={wopi_src}", flush=True) - - return access_token, access_token_ttl, wopi_src - - -def run_validator(group: str, token: str, wopi_src: str, ttl: str, - secure: bool = False) -> int: - print(f"\nRunning testgroup [{group}] secure={secure}", flush=True) - cmd = [ - "docker", "run", "--rm", - "--workdir", "/app", - "--entrypoint", "/app/Microsoft.Office.WopiValidator", - VALIDATOR_IMAGE, - ] - if secure: - cmd.append("-s") - cmd += ["-t", token, "-w", wopi_src, "-l", ttl, "--testgroup", group] - return subprocess.run(cmd).returncode - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--type", choices=["builtin", "cs3"], required=True, - help="WOPI server type: builtin (collaboration service) or cs3 (cs3org/wopiserver)") - args = parser.parse_args() - wopi_type = args.type - - repo_root = Path(__file__).resolve().parents[2] - ocis_bin = repo_root / "ocis/bin/ocis" - ocis_config_dir = Path.home() / ".ocis/config" - - subprocess.run(["make", "-C", str(repo_root / "ocis"), "build"], check=True) - - bridge_ip = get_docker_bridge_ip() - print(f"Docker bridge IP: {bridge_ip}", flush=True) - - procs = [] - containers = [] - - def cleanup(*_): - for p in procs: - try: - p.terminate() - except Exception: - pass - for name in containers: - subprocess.run(["docker", "rm", "-f", name], capture_output=True) - - signal.signal(signal.SIGTERM, cleanup) - signal.signal(signal.SIGINT, cleanup) - - try: - # --- fakeoffice: serves hosting-discovery.xml on :8080 --- - # Mirrors the old fakeOffice() step — owncloudci/alpine running serve-hosting-discovery.sh. - containers.append("wopi-fakeoffice-fake") - subprocess.run(["docker", "rm", "-f", "wopi-fakeoffice-fake"], capture_output=True) - subprocess.run([ - "docker", "run", "-d", "--name", "wopi-fakeoffice-fake", - "-p", "8080:8080", - "-v", f"{repo_root}:/ocis", - FAKEOFFICE_IMAGE, - "sh", "/ocis/tests/config/ci/serve-hosting-discovery.sh", - ], check=True) - - wait_for(lambda: tcp_reachable(bridge_ip, 8080), 60, "fakeoffice:8080") - print("fakeoffice ready.", flush=True) - - # --- Init and start OCIS --- - ocis_public_url = f"https://{bridge_ip}:9200" - server_env = {**os.environ} - server_env.update(base_server_env( - repo_root, str(ocis_config_dir), ocis_public_url, bridge_ip, wopi_type)) - - subprocess.run( - [str(ocis_bin), "init", "--insecure", "true"], - env=server_env, check=True, - ) - shutil.copy( - repo_root / "tests/config/ci/app-registry.yaml", - ocis_config_dir / "app-registry.yaml", - ) - - print("Starting ocis...", flush=True) - ocis_proc = subprocess.Popen([str(ocis_bin), "server"], env=server_env) - procs.append(ocis_proc) - - wait_for(lambda: ocis_healthy(OCIS_URL), 300, "ocis") - print("ocis ready.", flush=True) - - # --- Wait for fakeoffice discovery endpoint before starting WOPI service --- - # ocis collaboration server calls GetAppURLs synchronously at startup; - # if /hosting/discovery returns non-200, the process exits immediately. - wait_for(lambda: wopi_discovery_ready("http://127.0.0.1:8080/hosting/discovery"), - 300, "fakeoffice /hosting/discovery") - print("fakeoffice discovery ready.", flush=True) - - # --- Start wopi server (after OCIS is healthy so NATS/gRPC are up) --- - if wopi_type == "builtin": - # Run 'ocis collaboration server' as a host process. - # Mirrors drone wopiCollaborationService("fakeoffice") → startOcisService("collaboration"). - collab_env = {**os.environ} - collab_env.update(collab_service_env(bridge_ip, str(ocis_config_dir))) - print("Starting collaboration service...", flush=True) - collab_proc = subprocess.Popen( - [str(ocis_bin), "collaboration", "server"], - env=collab_env, - ) - procs.append(collab_proc) - else: - # cs3: patch wopiserver.conf (replace container hostname with bridge_ip), - # then run cs3org/wopiserver as a Docker container. - conf_text = (repo_root / "tests/config/ci/wopiserver.conf").read_text() - conf_text = conf_text.replace("ocis-server", bridge_ip) - conf_tmp = Path("/tmp/wopiserver-patched.conf") - conf_tmp.write_text(conf_text) - secret_tmp = Path("/tmp/wopisecret") - secret_tmp.write_text("123\n") - - containers.append("wopi-cs3server") - subprocess.run(["docker", "rm", "-f", "wopi-cs3server"], capture_output=True) - subprocess.run([ - "docker", "run", "-d", "--name", "wopi-cs3server", - "-p", "9300:9300", - "-v", f"{conf_tmp}:/etc/wopi/wopiserver.conf", - "-v", f"{secret_tmp}:/etc/wopi/wopisecret", - "--entrypoint", "/app/wopiserver.py", - CS3_WOPI_IMAGE, - ], check=True) - - wait_for(lambda: tcp_reachable(bridge_ip, 9300), 120, "wopi-fakeoffice:9300") - print("wopi server ready.", flush=True) - - # --- prepare-test-file: upload file, get WOPI credentials --- - access_token, ttl, wopi_src = prepare_test_file(bridge_ip) - - # --- Run validator for each testgroup --- - failed = [] - for group in SHARED_TESTGROUPS: - rc = run_validator(group, access_token, wopi_src, ttl, secure=False) - if rc != 0: - failed.append(group) - - if wopi_type == "builtin": - for group in BUILTIN_ONLY_TESTGROUPS: - rc = run_validator(group, access_token, wopi_src, ttl, secure=True) - if rc != 0: - failed.append(group) - - if failed: - print(f"\nFailed testgroups: {', '.join(failed)}", file=sys.stderr) - return 1 - print("\nAll WOPI validator tests passed.") - return 0 - - finally: - cleanup() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/config/ci/fakeoffice-server.py b/tests/config/ci/fakeoffice-server.py new file mode 100644 index 00000000000..db7a9c88091 --- /dev/null +++ b/tests/config/ci/fakeoffice-server.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Minimal HTTP server that serves hosting-discovery.xml on :8080.""" +import http.server +import pathlib + +body = pathlib.Path('/ocis/tests/config/ci/hosting-discovery.xml').read_bytes() + + +class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-Type', 'text/xml') + self.end_headers() + self.wfile.write(body) + + def log_message(self, *a): + pass + + +http.server.HTTPServer(('', 8080), Handler).serve_forever() diff --git a/tests/config/ci/onlyoffice-entrypoint.sh b/tests/config/ci/onlyoffice-entrypoint.sh new file mode 100644 index 00000000000..bd85fa0752e --- /dev/null +++ b/tests/config/ci/onlyoffice-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +cp /tmp/only-office.json /etc/onlyoffice/documentserver/local.json +openssl req -x509 -newkey rsa:4096 -keyout onlyoffice.key -out onlyoffice.crt -sha256 -days 365 -batch -nodes +mkdir -p /var/www/onlyoffice/Data/certs +cp onlyoffice.key /var/www/onlyoffice/Data/certs/ +cp onlyoffice.crt /var/www/onlyoffice/Data/certs/ +chmod 400 /var/www/onlyoffice/Data/certs/onlyoffice.key +/app/ds/run-document-server.sh