Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/server-compatibility.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/composer.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
19 changes: 19 additions & 0 deletions tests/integration/config/behat.yml
Original file line number Diff line number Diff line change
@@ -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/'
99 changes: 99 additions & 0 deletions tests/integration/features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

use Behat\Step\Given;
use Behat\Step\When;
use GuzzleHttp\Client as HttpClient;
use LibreCode\UsageStatistics\Client;
use LibreCode\UsageStatistics\ConsentState;
use LibreCode\UsageStatistics\Endpoint;
use LibreCode\UsageStatistics\InstallationId;
use LibreCode\UsageStatistics\Metric;
use LibreCode\UsageStatistics\Report;
use LibreCode\UsageStatistics\ReportingPeriod;
use LibreCode\UsageStatistics\SubmissionResult;
use LibreCode\UsageStatistics\Transport\Response;
use LibreCode\UsageStatistics\Transport\TransportInterface;
use Libresign\NextcloudBehat\NextcloudApiContext;

final class FeatureContext extends NextcloudApiContext {
#[Given('as anonymous user')]
public function asAnonymousUser(): void {
$this->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.');
}
}
}
30 changes: 30 additions & 0 deletions tests/integration/features/client_server_compatibility.feature
Original file line number Diff line number Diff line change
@@ -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 |
Loading