Skip to content
Draft
82 changes: 82 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT

name: Integration tests

on:
pull_request:
paths:
- .github/workflows/integration.yml
- appinfo/**
- composer.*
- lib/**
- templates/**
- tests/**
push:
branches:
- main
- stable*
- test
paths:
- .github/workflows/integration.yml
- appinfo/**
- composer.*
- lib/**
- templates/**
- tests/**

env:
APP_NAME: integration_github

jobs:
integration:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
php-versions: ['8.2']
databases: ['sqlite']
server-versions: ['master']

name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}

steps:
- name: Checkout server
uses: actions/checkout@v4
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
submodules: true

- name: Checkout app
uses: actions/checkout@v4
with:
path: apps/${{ env.APP_NAME }}

- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit:11.x
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, gd, zip

- name: Set up PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: composer i

- name: Set up Nextcloud
run: |
mkdir data
./occ maintenance:install --verbose --database=sqlite --admin-user admin --admin-pass admin
./occ app:enable --force ${{ env.APP_NAME }}

- name: PHPUnit integration
working-directory: apps/${{ env.APP_NAME }}
env:
CI_CLIENT_ID: ${{ secrets.CI_CLIENT_ID }}
CI_CLIENT_SECRET: ${{ secrets.CI_CLIENT_SECRET }}
CI_USER_LOGIN: ${{ secrets.CI_USER_LOGIN }}
CI_USER_PASSWORD: ${{ secrets.CI_USER_PASSWORD }}
CI_TOTP_SECRET: ${{ secrets.CI_TOTP_SECRET }}
run: composer run test:integration
34 changes: 17 additions & 17 deletions .github/workflows/phpunit-mysql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,23 +117,23 @@ jobs:
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit

- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep "^ test:integration " | wc -l | grep 1

- name: Run Nextcloud
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &

- name: PHPUnit integration
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
#- name: Check PHPUnit integration script is defined
# id: check_integration
# continue-on-error: true
# working-directory: apps/${{ env.APP_NAME }}
# run: |
# composer run --list | grep "^ test:integration " | wc -l | grep 1

#- name: Run Nextcloud
# # Only run if phpunit integration config file exists
# if: steps.check_integration.outcome == 'success'
# run: php -S localhost:8080 &

#- name: PHPUnit integration
# # Only run if phpunit integration config file exists
# if: steps.check_integration.outcome == 'success'
# working-directory: apps/${{ env.APP_NAME }}
# run: composer run test:integration

- name: Skipped
# Fail the action when neither unit nor integration tests ran
Expand Down
34 changes: 17 additions & 17 deletions .github/workflows/phpunit-pgsql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,23 @@ jobs:
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit

- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep "^ test:integration " | wc -l | grep 1

- name: Run Nextcloud
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &

- name: PHPUnit integration
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
#- name: Check PHPUnit integration script is defined
# id: check_integration
# continue-on-error: true
# working-directory: apps/${{ env.APP_NAME }}
# run: |
# composer run --list | grep "^ test:integration " | wc -l | grep 1

#- name: Run Nextcloud
# # Only run if phpunit integration config file exists
# if: steps.check_integration.outcome == 'success'
# run: php -S localhost:8080 &

#- name: PHPUnit integration
# # Only run if phpunit integration config file exists
# if: steps.check_integration.outcome == 'success'
# working-directory: apps/${{ env.APP_NAME }}
# run: composer run test:integration

- name: Print logs
if: always()
Expand Down
34 changes: 17 additions & 17 deletions .github/workflows/phpunit-sqlite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,23 +104,23 @@ jobs:
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit

- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep "^ test:integration " | wc -l | grep 1

- name: Run Nextcloud
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &

- name: PHPUnit integration
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
#- name: Check PHPUnit integration script is defined
# id: check_integration
# continue-on-error: true
# working-directory: apps/${{ env.APP_NAME }}
# run: |
# composer run --list | grep "^ test:integration " | wc -l | grep 1

#- name: Run Nextcloud
# # Only run if phpunit integration config file exists
# if: steps.check_integration.outcome == 'success'
# run: php -S localhost:8080 &

#- name: PHPUnit integration
# # Only run if phpunit integration config file exists
# if: steps.check_integration.outcome == 'success'
# working-directory: apps/${{ env.APP_NAME }}
# run: composer run test:integration

- name: Print logs
if: always()
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm.phar --no-cache",
"test:unit": "phpunit -c tests/phpunit.xml --no-coverage",
"test:integration": "phpunit -c tests/phpunit.integration.xml --no-coverage",
"post-install-cmd": [
"@composer bin all install --ansi",
"composer dump-autoload"
Expand Down
12 changes: 10 additions & 2 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
<?php

use OCA\Github\AppInfo\Application;
use OCP\App\IAppManager;
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

// prevent loading all apps because loading files_external causes oc_external_mounts to be queried
// but it does not exists because this app is not necessarily enabled in the local test environments
putenv('TEST_DONT_LOAD_APPS=1');
require_once __DIR__ . '/../../../tests/bootstrap.php';

use OCA\Github\AppInfo\Application;
use OCP\App\IAppManager;

\OCP\Server::get(IAppManager::class)->loadApp(Application::APP_ID);
OC_Hook::clear();
90 changes: 90 additions & 0 deletions tests/integration/GitHubHtml.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Github\Tests\Integration;

use DOMDocument;
use DOMElement;
use DOMXPath;

final class GitHubHtml {

public static function loadXPath(string $body): DOMXPath {
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML($body);

return new DOMXPath($doc);
}

public static function getPageTitle(DOMXPath $selector): string {
$title = $selector->query('//title')?->item(0)?->textContent;
return $title === null ? 'no title' : trim($title);
}

public static function findForm(DOMXPath $selector, array $formSelectors): ?DOMElement {
foreach ($formSelectors as $formSelector) {
$result = $selector->query($formSelector);
$form = $result?->item(0);
if ($form instanceof DOMElement) {
return $form;
}
}

return null;
}

public static function findAuthorizeForm(DOMXPath $selector): ?DOMElement {
return self::findForm($selector, [
'//form[contains(@class, "js-oauth-authorize-form")]',
'//form[contains(@class, "js-oath-authorize-form")]',
'//form[@action="/login/oauth/authorize"]',
'//form[contains(@action, "authorize")]',
]);
}

public static function findTwoFactorForm(DOMXPath $selector): ?DOMElement {
return self::findForm($selector, [
'//form[contains(@action, "two-factor") and .//input[@name="app_otp" or @name="otp"]]',
'//form[.//input[@name="app_otp" or @name="otp"]]',
]);
}

public static function resolveUrl(string $url, string $fallbackUrl): string {
if ($url === '') {
return $fallbackUrl;
}
if (str_starts_with($url, 'http://') || str_starts_with($url, 'https://')) {
return $url;
}
if (str_starts_with($url, '/')) {
return 'https://github.com' . $url;
}

$baseUrl = preg_replace('/[^\/]+$/', '', $fallbackUrl);
return $baseUrl . $url;
}

public static function extractFormInputs(DOMXPath $selector, DOMElement $form): array {
$formParams = [];
$inputs = $selector->query('.//input[@name] | .//button[@name]', $form);
foreach ($inputs as $input) {
$name = $input->getAttribute('name');
$value = $input->getAttribute('value');
$type = $input->getAttribute('type');
if ($type === 'checkbox' && !$input->hasAttribute('checked')) {
continue;
}
$formParams[$name] = $value;
}
libxml_clear_errors();

return $formParams;
}
}
Loading
Loading