From cc14ec53ee01835480d12b6218e6d4ebfab89dfe Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Mon, 11 May 2026 09:54:35 +0200 Subject: [PATCH] ci(api-tests): add API test to CI --- .github/workflows/docker-build.yml | 6 +- .github/workflows/end-to-end-tests.yml | 110 +++++++++++++++++++++++++ .github/workflows/playwright.yml | 61 -------------- tests/api/README.md | 14 ++-- tests/api/contacts.hurl | 34 ++++++-- tests/api/run.sh | 8 +- tests/api/test.env | 6 ++ tests/common/server.env | 2 +- 8 files changed, 158 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/end-to-end-tests.yml delete mode 100644 .github/workflows/playwright.yml create mode 100644 tests/api/test.env diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 54dbadb7..2c9c71ca 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -6,9 +6,9 @@ on: pull_request: branches: [ "main", "dev" ] -concurrency: - group: rust-build-${{ github.ref }} - cancel-in-progress: false +#concurrency: +# group: rust-build-${{ github.ref }} +# cancel-in-progress: false jobs: build-and-test: diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml new file mode 100644 index 00000000..7b9aa86f --- /dev/null +++ b/.github/workflows/end-to-end-tests.yml @@ -0,0 +1,110 @@ +name: Playwright Test (end-to-end) + +# IMPORTANT: using Swatinem/rust-cache@v2 : reuse same cache as ci.yml (minimize non necessary new compilation) + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +#concurrency: +# group: rust-build-${{ github.ref }} +# cancel-in-progress: false + +jobs: + api-test: + name: Hurl API tests + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache compiled binary + id: binary-cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/target/debug/oxicloud + key: ${{ runner.os }}-oxicloud-binary-${{ hashFiles('src/**', 'Cargo.toml', 'Cargo.lock') }} + + - name: Cache Rust dependencies + if: steps.binary-cache.outputs.cache-hit != 'true' + uses: Swatinem/rust-cache@v2 + + - name: Build server + if: steps.binary-cache.outputs.cache-hit != 'true' + run: cargo build + + - name: Install Hurl + env: + HURL_MAJOR: "8" + run: | + HURL_VERSION=$(curl -fsSL -H "Authorization: Bearer ${{ github.token }}" \ + https://api.github.com/repos/Orange-OpenSource/hurl/releases \ + | jq -r "map(select(.tag_name | startswith(\"${HURL_MAJOR}.\"))) | first | .tag_name") + curl -fLO "https://github.com/Orange-OpenSource/hurl/releases/download/${HURL_VERSION}/hurl_${HURL_VERSION}_amd64.deb" + sudo apt-get install -y "./hurl_${HURL_VERSION}_amd64.deb" + + - name: Run Hurl API tests + run: bash tests/api/run.sh + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: hurl-report + path: tests/api/storage/ + retention-days: 7 + + front-test: + # starts api-test first + needs: api-test + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache compiled binary + id: binary-cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/target/debug/oxicloud + key: ${{ runner.os }}-oxicloud-binary-${{ hashFiles('src/**', 'Cargo.toml', 'Cargo.lock') }} + + - name: Cache Rust dependencies + if: steps.binary-cache.outputs.cache-hit != 'true' + uses: Swatinem/rust-cache@v2 + + - name: Build server + if: steps.binary-cache.outputs.cache-hit != 'true' + run: cargo build + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install Node dependencies + working-directory: tests/e2e + run: npm ci + + - name: Install Playwright browsers + working-directory: tests/e2e + run: npx playwright install --with-deps + + - name: Run Playwright tests (spawns DB via pretest hook) + working-directory: tests/e2e + run: npm test + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: tests/e2e/playwright-report/ + retention-days: 30 + + diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index 999950f5..00000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Playwright Test (end-to-end) - -# IMPORTANT: using Swatinem/rust-cache@v2 : reuse same cache as ci.yml (minimize non necessary new compilation) - -on: - push: - branches: [ main, dev ] - pull_request: - branches: [ main, dev ] - -concurrency: - group: rust-build-${{ github.ref }} - cancel-in-progress: false - -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Cache compiled binary - id: binary-cache - uses: actions/cache@v4 - with: - path: ${{ github.workspace }}/target/debug/oxicloud - key: ${{ runner.os }}-oxicloud-binary-${{ hashFiles('src/**', 'Cargo.toml', 'Cargo.lock') }} - - - name: Cache Rust dependencies - if: steps.binary-cache.outputs.cache-hit != 'true' - uses: Swatinem/rust-cache@v2 - - - name: Build server - if: steps.binary-cache.outputs.cache-hit != 'true' - run: cargo build - - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - - name: Install Node dependencies - working-directory: tests/e2e - run: npm ci - - - name: Install Playwright browsers - working-directory: tests/e2e - run: npx playwright install --with-deps - - - name: Run Playwright tests (spawns DB via pretest hook) - working-directory: tests/e2e - run: npm test - - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: tests/e2e/playwright-report/ - retention-days: 30 diff --git a/tests/api/README.md b/tests/api/README.md index d4aa2994..62be5dea 100644 --- a/tests/api/README.md +++ b/tests/api/README.md @@ -9,7 +9,7 @@ Tests are written using [Hurl](https://hurl.dev) — a plain-text, CLI-first HTT ## Configuration -Edit `.env` to match your local instance. This file is the single source of +Edit `test.env` to match your local instance. This file is the single source of truth: `run.sh` sources it for shell variables and passes it to Hurl as `--variables-file`. @@ -24,19 +24,19 @@ password=TestPassword1! ```bash # First-time setup (run once on a fresh instance) -hurl --variables-file tests/api/hurl.vars --test tests/api/setup.hurl +hurl --variables-file tests/api/test.env --test tests/api/setup.hurl # Contacts CRUD scenario -hurl --variables-file tests/api/hurl.vars --test tests/api/contacts.hurl +hurl --variables-file tests/api/test.env --test tests/api/contacts.hurl # All scenarios at once -hurl --variables-file tests/api/hurl.vars --test tests/api/setup.hurl tests/api/contacts.hurl +hurl --variables-file tests/api/test.env --test tests/api/setup.hurl tests/api/contacts.hurl # With full request/response output -hurl --variables-file tests/api/hurl.vars --test --verbose tests/api/contacts.hurl +hurl --variables-file tests/api/test.env --test --verbose tests/api/contacts.hurl # Generate an HTML report -hurl --variables-file tests/api/hurl.vars --test --report-html /tmp/hurl-report tests/api/contacts.hurl +hurl --variables-file tests/api/test.env --test --report-html /tmp/hurl-report tests/api/contacts.hurl ``` ## Test files @@ -45,7 +45,7 @@ hurl --variables-file tests/api/hurl.vars --test --report-html /tmp/hurl-report |---|---| | `setup.hurl` | One-time admin account creation; also asserts the endpoint is locked afterwards | | `contacts.hurl` | Full contacts CRUD scenario (13 steps, see below) | -| `.env` | Variables: `base_url`, `username`, `email`, `password` — used by both Hurl and `run.sh` | +| `test.env` | Variables: `base_url`, `username`, `email`, `password` — used by both Hurl and `run.sh` | ## Scenario: `contacts.hurl` diff --git a/tests/api/contacts.hurl b/tests/api/contacts.hurl index cc49855a..2570668d 100644 --- a/tests/api/contacts.hurl +++ b/tests/api/contacts.hurl @@ -165,6 +165,8 @@ jsonpath "$.phone[0].type" == "mobile" # ───────────────────────────────────────────────────────────── # Step 8 – Update John Doe: add nickname, notes, and organisation # Uses If-Match for optimistic concurrency +# Captures the refreshed ETag into etag_updated so that +# the original etag remains available as a stale value. # ───────────────────────────────────────────────────────────── PUT {{base_url}}/api/address-books/{{book_id}}/contacts/{{contact_id}} Authorization: Bearer {{token}} @@ -195,7 +197,7 @@ If-Match: {{etag}} HTTP 200 [Captures] -etag: header "ETag" +etag_updated: header "ETag" [Asserts] header "ETag" exists jsonpath "$.id" == {{contact_id}} @@ -207,17 +209,35 @@ jsonpath "$.notes" == "Updated via Hurl test" # ───────────────────────────────────────────────────────────── -# Step 9 – Delete John Doe (uses refreshed ETag from step 8) +# Step 9 – Stale ETag rejection: update with the pre-step-8 ETag +# The contact was already modified so this must return 412 +# ───────────────────────────────────────────────────────────── +PUT {{base_url}}/api/address-books/{{book_id}}/contacts/{{contact_id}} +Authorization: Bearer {{token}} +Content-Type: application/json +If-Match: {{etag}} +{ + "first_name": "John", + "last_name": "Doe", + "full_name": "John Doe", + "nickname": "should-not-be-saved" +} + +HTTP 412 + + +# ───────────────────────────────────────────────────────────── +# Step 10 – Delete John Doe (uses refreshed ETag from step 8) # ───────────────────────────────────────────────────────────── DELETE {{base_url}}/api/address-books/{{book_id}}/contacts/{{contact_id}} Authorization: Bearer {{token}} -If-Match: {{etag}} +If-Match: {{etag_updated}} HTTP 204 # ───────────────────────────────────────────────────────────── -# Step 10 – Address book must be empty again +# Step 11 – Address book must be empty again # ───────────────────────────────────────────────────────────── GET {{base_url}}/api/address-books/{{book_id}}/contacts Authorization: Bearer {{token}} @@ -228,7 +248,7 @@ jsonpath "$" count == 0 # ───────────────────────────────────────────────────────────── -# Step 11 – Delete the personal address book +# Step 12 – Delete the personal address book # ───────────────────────────────────────────────────────────── DELETE {{base_url}}/api/address-books/{{book_id}} Authorization: Bearer {{token}} @@ -237,7 +257,7 @@ HTTP 204 # ───────────────────────────────────────────────────────────── -# Step 12 – Verify the address book is gone from the list +# Step 13 – Verify the address book is gone from the list # ───────────────────────────────────────────────────────────── GET {{base_url}}/api/address-books Authorization: Bearer {{token}} @@ -248,7 +268,7 @@ jsonpath "$[*].id" not contains {{book_id}} # ───────────────────────────────────────────────────────────── -# Step 13 – List the system address book (OxiCloud users) +# Step 14 – List the system address book (OxiCloud users) # ───────────────────────────────────────────────────────────── GET {{base_url}}/api/address-books/system/contacts Authorization: Bearer {{token}} diff --git a/tests/api/run.sh b/tests/api/run.sh index 5808c422..779c44dd 100755 --- a/tests/api/run.sh +++ b/tests/api/run.sh @@ -13,9 +13,9 @@ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" COMMON="$REPO_ROOT/tests/common" API_DIR="$REPO_ROOT/tests/api" -# .env is the single source of truth for connection details and credentials. -# shellcheck source=.env -source "$API_DIR/.env" +# test.env is the single source of truth for connection details and credentials. +# shellcheck source=test.env +source "$API_DIR/test.env" # Derive server port from base_url (e.g. http://localhost:8087 → 8087) SERVER_PORT="${base_url##*:}" @@ -77,7 +77,7 @@ log "Server is ready." # ── 4. Run Hurl tests ───────────────────────────────────────────────────────── log "Running Hurl tests..." -hurl --variables-file "$API_DIR/.env" --test --jobs 1 \ +hurl --variables-file "$API_DIR/test.env" --test --jobs 1 \ "$API_DIR/setup.hurl" \ "$API_DIR/contacts.hurl" diff --git a/tests/api/test.env b/tests/api/test.env new file mode 100644 index 00000000..9e12d698 --- /dev/null +++ b/tests/api/test.env @@ -0,0 +1,6 @@ +# Test credentials for local/CI API tests — NOT real secrets. +base_url=http://localhost:8087 +username=admin +email=admin@example.com +# gitguardian:ignore +password=TestPassword1! diff --git a/tests/common/server.env b/tests/common/server.env index 990c30cd..65e1a78b 100644 --- a/tests/common/server.env +++ b/tests/common/server.env @@ -15,4 +15,4 @@ OXICLOUD_ENABLE_MUSIC=true OXICLOUD_EXPOSE_SYSTEM_USERS=true OXICLOUD_WOPI_ENABLED=false OXICLOUD_OIDC_ENABLED=false -RUST_LOG=info +RUST_LOG=warn