diff --git a/.github/workflows/server-compatibility.yml b/.github/workflows/server-compatibility.yml new file mode 100644 index 0000000..487c5ec --- /dev/null +++ b/.github/workflows/server-compatibility.yml @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Server compatibility + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: server-compatibility-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + compatibility: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout client + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Checkout Nextcloud + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + repository: nextcloud/server + ref: stable35 + path: build/nextcloud + submodules: true + + - name: Checkout Usage Statistics Server + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + repository: LibreCodeCoop/usage_statistics_server + ref: main + path: build/nextcloud/apps/usage_statistics_server + + - name: Set up PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 + with: + php-version: '8.3' + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, sockets, sqlite, pdo_sqlite, xmlreader, xmlwriter, zip, zlib + coverage: none + ini-file: development + ini-values: disable_functions= + + - name: Install server dependencies + working-directory: build/nextcloud/apps/usage_statistics_server + run: | + composer remove nextcloud/ocp --dev --no-scripts + composer install --no-dev --no-scripts --prefer-dist --no-progress + + - name: Install compatibility test dependencies + working-directory: tests/integration + run: composer install --prefer-dist --no-progress + + - name: Set up Nextcloud + working-directory: build/nextcloud + run: | + mkdir data + ./occ maintenance:install \ + --verbose \ + --database=sqlite \ + --database-name=nextcloud \ + --admin-user admin \ + --admin-pass admin + ./occ app:enable --force usage_statistics_server + ./occ config:system:set auth.bruteforce.protection.enabled --value false --type boolean + ./occ config:system:set ratelimit.protection.enabled --value false --type boolean + ./occ config:system:set debug --value true --type boolean + + - name: Run client/server compatibility test + working-directory: tests/integration + env: + BEHAT_ROOT_DIR: ../../build/nextcloud + BEHAT_RUN_AS: runner + BEHAT_VERBOSE: ${{ runner.debug }} + run: | + mkdir -p output + set -o pipefail + composer run behat 2>&1 | tee output/compatibility.log + + - name: Upload Behat results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: server-compatibility-results + path: | + tests/integration/output/ + build/nextcloud/data/nextcloud.log + if-no-files-found: ignore + retention-days: 30 + + - name: Print Nextcloud logs + if: always() + run: cat build/nextcloud/data/nextcloud.log 2>/dev/null || true diff --git a/REUSE.toml b/REUSE.toml index 8f4b9e0..7be64cb 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -15,7 +15,8 @@ path = [ "phpunit.xml.dist", "phpmd.xml", "psalm.xml", - "vendor-bin/**/composer.json" + "vendor-bin/**/composer.json", + "tests/integration/composer.json" ] precedence = "aggregate" SPDX-FileCopyrightText = "2026 LibreCode coop and contributors" diff --git a/tests/integration/composer.json b/tests/integration/composer.json new file mode 100644 index 0000000..d9b402c --- /dev/null +++ b/tests/integration/composer.json @@ -0,0 +1,31 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../..", + "options": { + "symlink": true + } + } + ], + "require": { + "behat/behat": "3.32.0", + "guzzlehttp/guzzle": "7.15.3", + "jarnaiz/behat-junit-formatter": "1.3.2", + "librecodecoop/usage-statistics-client": "@dev", + "libresign/nextcloud-behat": "1.6.0", + "php-http/guzzle7-adapter": "1.1.0", + "php-http/message": "1.16.2" + }, + "scripts": { + "behat": "@php -d error_reporting=24575 vendor/bin/behat -f junit -f pretty --colors" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + }, + "platform": { + "php": "8.3" + } + } +} diff --git a/tests/integration/config/behat.yml b/tests/integration/config/behat.yml new file mode 100644 index 0000000..c1c32e3 --- /dev/null +++ b/tests/integration/config/behat.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +default: + autoload: + '': '%paths.base%/../features/bootstrap' + suites: + default: + contexts: + - FeatureContext + paths: + - '%paths.base%/../features' + + extensions: + PhpBuiltin\Server: + workers: 2 + jarnaiz\JUnitFormatter\JUnitFormatterExtension: + filename: report.xml + outputDir: '%paths.base%/../output/' diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..e0450f0 --- /dev/null +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -0,0 +1,99 @@ +setCurrentUser(''); + } + + #[When('the usage statistics client submits the compatibility report')] + public function submitCompatibilityReport(): void { + $report = new Report( + application: 'client_server_e2e', + installationId: (string)InstallationId::derive('client_server_e2e', 'integration-installation'), + schemaVersion: 1, + period: new ReportingPeriod( + new \DateTimeImmutable('2026-09-01T00:00:00Z'), + new \DateTimeImmutable('2026-10-01T00:00:00Z'), + ), + metrics: [ + Metric::string('environment', 'version', '1.2.3'), + Metric::integer('usage', 'requests_completed', 17), + ], + ); + + $transport = new class($this->baseUrl) implements TransportInterface { + public function __construct( + private readonly string $baseUrl, + ) { + } + + public function request( + string $method, + string $url, + array $headers, + string $body, + float $timeoutSeconds, + ): Response { + $path = parse_url($url, PHP_URL_PATH); + if (!is_string($path) || $path === '') { + throw new \RuntimeException('Compatibility endpoint path is invalid.'); + } + + $http = new HttpClient(['http_errors' => false]); + $response = $http->request( + $method, + rtrim($this->baseUrl, '/') . $path, + [ + 'headers' => $headers, + 'body' => $body, + 'timeout' => $timeoutSeconds, + ], + ); + + $responseHeaders = []; + foreach ($response->getHeaders() as $name => $values) { + $responseHeaders[$name] = implode(', ', $values); + } + + return new Response( + $response->getStatusCode(), + (string)$response->getBody(), + $responseHeaders, + ); + } + }; + + $client = new Client( + $transport, + new Endpoint('https://usage-statistics.invalid/index.php/apps/usage_statistics_server/api/v1/reports'), + ); + + $result = $client->submit($report, ConsentState::Enabled); + if ($result !== SubmissionResult::Accepted) { + throw new \RuntimeException('Compatibility report was not accepted.'); + } + } +} diff --git a/tests/integration/features/client_server_compatibility.feature b/tests/integration/features/client_server_compatibility.feature new file mode 100644 index 0000000..198e71b --- /dev/null +++ b/tests/integration/features/client_server_compatibility.feature @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +Feature: Usage Statistics Protocol v1 client/server compatibility + + Scenario: the real client submits a report that the server stores and aggregates + Given as user "admin" + When sending "post" to ocs "/apps/usage_statistics_server/api/v1/admin/schemas" + | application | client_server_e2e | + | schemaVersion | 1 | + | metrics | [{"category":"environment","key":"version","type":"string","kind":"snapshot","aggregation":"distribution","description":"Application version","required":true},{"category":"usage","key":"requests_completed","type":"integer","kind":"period","aggregation":"numerical","description":"Completed requests","required":true}] | + Then the response should have a status code 201 + + When the usage statistics client submits the compatibility report + + Given as user "admin" + When sending "get" to ocs "/apps/usage_statistics_server/api/v1/admin/applications/client_server_e2e/metrics/environment/version/distribution" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.values[0].value | 1.2.3 | + | (jq).ocs.data.values[0].count | 1 | + + When sending "get" to ocs "/apps/usage_statistics_server/api/v1/admin/applications/client_server_e2e/metrics/usage/requests_completed/numerical" + Then the response should have a status code 200 + And the response should be a JSON array with the following mandatory values + | key | value | + | (jq).ocs.data.statistics.count | 1 | + | (jq).ocs.data.statistics.average | 17 | + | (jq).ocs.data.statistics.total | 17 |