Files
nextcloud-mcp-server/.github/workflows/test.yml
T
Chris Coutinho f43343356e fix: address review feedback — security, caching, CI 429 retry
- Add 429 retry with exponential backoff to register_client() (fixes CI
  oauth matrix failures from parallel DCR requests)
- Make client_id, redirect_uri, and PKCE mandatory at token endpoint
- Add null-checks for discovery_url and OAuth credentials in proxy flows
- Add OIDC discovery document caching with 5-min TTL
- Add per-IP rate limiting on /oauth/register DCR proxy
- Discover DCR endpoint from OIDC discovery instead of hardcoding
- Extract extract_user_id_from_token to auth/token_utils.py (breaks
  circular imports between server/ and auth/ layers)
- Add TTL scope cache in scope_authorization.py (avoids DB hit per tool)
- Add defense-in-depth scope validation in storage layer
- Broaden elicitation exception handling with graceful fallback
- Add idempotentHint to nc_auth_check_status, return "pending" status
  after accepted elicitation, add polling interval to description
- Change ALL_SUPPORTED_SCOPES from tuple to frozenset for O(1) lookups
- Replace Optional[str] with str | None throughout config.py
- Use default_factory for ProxyCodeEntry/ASProxySession dataclasses
- Add proxy code/session cleanup to background loop
- Fix OIDC verification CI step to only run for oauth/login-flow modes
- Add unit tests for access.py REST endpoints (10 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:22:23 +01:00

214 lines
7.9 KiB
YAML

name: Tests
on:
pull_request:
branches:
- master
jobs:
linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
- name: Check format
run: uv run --frozen ruff format --diff
- name: Linting
run: uv run --frozen ruff check
- name: Type check
run: uv run --frozen ty check -- nextcloud_mcp_server
unit-test:
runs-on: ubuntu-latest
needs: [linting]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
- name: Run unit tests
run: uv run pytest -v -m unit -o "addopts=-p no:asyncio"
integration-test:
runs-on: ubuntu-latest
needs: [linting]
strategy:
fail-fast: false
matrix:
nextcloud_version:
- "31"
- "32"
# - "33" # Disabled until all upstream apps support NC 33
mode:
- "single-user"
- "multi-user-basic"
- "oauth"
- "login-flow"
include:
# Version-specific image pins — Renovate updates these via customManagers
# renovate: datasource=docker depName=docker.io/library/nextcloud
- nextcloud_version: "31"
nextcloud_image: "docker.io/library/nextcloud:31.0.8@sha256:92bc503ea0c19789f402b0469ecfb8df1ffea81e2bf90a45bba39063a626aa00"
# renovate: datasource=docker depName=docker.io/library/nextcloud
- nextcloud_version: "32"
nextcloud_image: "docker.io/library/nextcloud:32.0.6@sha256:5c4e09f72f096cd68379a8ae69f71e61d13da5a07430fc4a17c702a14e6a4267"
# renovate: datasource=docker depName=docker.io/library/nextcloud
# Disabled until all upstream apps support NC 33
# - nextcloud_version: "33"
# nextcloud_image: "docker.io/library/nextcloud:33.0.0@sha256:ff2cbaab14c85e587b5541e3aff4216a8a484e06424ebae661581937c0c8da0c"
# Mode-specific properties
- mode: single-user
profile: single-user
markers: "(smoke and not oauth and not keycloak and not login_flow and not multi_user_basic) or (integration and not oauth and not keycloak and not login_flow and not multi_user_basic)"
wait-port: 8000
mcp-internal-url: "http://mcp:8000"
needs-playwright: false
extra-args: >-
--ignore=tests/integration/test_qdrant_collection_creation.py
--ignore=tests/rag_evaluation/
- mode: multi-user-basic
profile: multi-user-basic
markers: "multi_user_basic"
wait-port: 8003
mcp-internal-url: "http://mcp-multi-user-basic:8000"
needs-playwright: true
extra-args: ""
- mode: oauth
profile: oauth
markers: "oauth and not keycloak"
wait-port: 8001
mcp-internal-url: "http://mcp-oauth:8001"
needs-playwright: true
extra-args: ""
- mode: login-flow
profile: login-flow
markers: "login_flow"
wait-port: 8004
mcp-internal-url: "http://mcp-login-flow:8004"
needs-playwright: true
extra-args: ""
name: integration (${{ matrix.mode }} / nc${{ matrix.nextcloud_version }})
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: 'true'
- name: Set up PHP 8.4
if: matrix.mode != 'single-user'
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # 2.36.0
with:
php-version: 8.4
coverage: none
# OIDC app installed from app store (dev mount removed from docker-compose.yml)
- name: Set up Node.js
if: matrix.mode != 'single-user'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- name: Build Astrolabe app
if: matrix.mode != 'single-user'
run: |
cd third_party/astrolabe
composer install --no-dev --optimize-autoloader
npm ci
npm run build
# Start services with the appropriate profile
- name: Run docker compose
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
with:
compose-file: "./docker-compose.yml"
compose-flags: "--profile ${{ matrix.profile }}"
up-flags: "--build"
env:
MCP_SERVER_URL: ${{ matrix.mcp-internal-url }}
NEXTCLOUD_IMAGE: ${{ matrix.nextcloud_image }}
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
- name: Install Playwright
if: matrix.needs-playwright
run: uv run playwright install chromium --with-deps
# Wait for Nextcloud to be healthy
- name: Wait for Nextcloud
run: |
echo "Waiting for Nextcloud at http://localhost:8080..."
max_attempts=60
attempt=0
until curl -sSf http://localhost:8080/status.php 2>/dev/null | grep -q '"installed":true'; do
attempt=$((attempt + 1))
if [ $attempt -ge $max_attempts ]; then
echo "Nextcloud did not become ready in time."
docker compose logs app
exit 1
fi
echo "Attempt $attempt/$max_attempts: Not ready, sleeping 5s..."
sleep 5
done
echo "Nextcloud is ready."
# Wait for the MCP service to be healthy
- name: Wait for MCP service (${{ matrix.mode }})
run: |
echo "Waiting for MCP service on port ${{ matrix.wait-port }}..."
max_attempts=30
attempt=0
until curl -o /dev/null -s -w "%{http_code}\n" http://localhost:${{ matrix.wait-port }}/health 2>/dev/null | grep -qE "200|404|405"; do
attempt=$((attempt + 1))
if [ $attempt -ge $max_attempts ]; then
echo "MCP service did not become ready in time."
docker compose --profile ${{ matrix.profile }} logs
exit 1
fi
echo "Attempt $attempt/$max_attempts: Not ready, sleeping 5s..."
sleep 5
done
echo "MCP service is ready on port ${{ matrix.wait-port }}."
- name: Verify OIDC configuration
if: matrix.mode == 'oauth' || matrix.mode == 'login-flow'
run: |
echo "=== OIDC Discovery ==="
curl -s http://localhost:8080/.well-known/openid-configuration | jq .
echo "=== OIDC App Status ==="
docker compose exec -T app php occ app:list --output=json 2>/dev/null | jq '.enabled.oidc // "NOT INSTALLED"'
- name: Run tests (${{ matrix.mode }})
env:
NEXTCLOUD_HOST: "http://localhost:8080"
NEXTCLOUD_USERNAME: "admin"
NEXTCLOUD_PASSWORD: "admin"
run: |
uv run pytest -v \
--log-cli-level=WARN \
-m '${{ matrix.markers }}' \
-o "addopts=-p no:asyncio" \
--timeout=300 \
${{ matrix.extra-args }}
- name: Collect service logs on failure
if: failure()
run: docker compose --profile ${{ matrix.profile }} logs --tail=500 > /tmp/docker-compose-logs.txt 2>&1
- name: Upload debug artifacts
if: failure()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: debug-${{ matrix.mode }}-nc${{ matrix.nextcloud_version }}
path: |
/tmp/*.png
/tmp/docker-compose-logs.txt
retention-days: 7
if-no-files-found: ignore