Skip to content
Open
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
22 changes: 18 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,20 @@ The app supports two OCR processing modes:

### Critical Context

**IMPORTANT**: This app cannot run standalone. For development, you MUST:
**IMPORTANT**: This app cannot *run* standalone, and most of its tests still need a real
Nextcloud instance. For development, you MUST:

1. Set up a full [Nextcloud Server](https://github.com/nextcloud/server) instance
2. Install this app into the Nextcloud installation
3. Tests must run within a working Nextcloud environment (see CI/CD workflows for examples)
3. `tests/Integration/` and the full `tests/Unit/` suite (`phpunit.xml`) must run within a
working Nextcloud environment (see CI/CD workflows for examples)

**Exception**: most of `tests/Unit/` (all but seven files that touch a real, non-trivial
Nextcloud implementation rather than just an interface) can run standalone — clone,
`composer install`, then `make php-unittest-standalone` — with no Nextcloud checkout,
using the `nextcloud/ocp` composer package for `OCP\*`/`NCU\*` symbols. See CLAUDE.md's
"Critical constraints" for
the excluded files and the `dev-master` dependency risk.

### Target Version Compatibility

Expand Down Expand Up @@ -244,11 +253,16 @@ Set Nextcloud log level to 0 for detailed debugging:

### General Guidelines

1. **Never assume standalone operation** - All development and testing requires a Nextcloud instance
1. **Never assume standalone operation for the app itself or for `tests/Integration/`** -
Running the app and its integration tests requires a Nextcloud instance. Most of
`tests/Unit/` is the one exception and runs standalone via
`make php-unittest-standalone` (see CLAUDE.md)
2. **Check Nextcloud Server code** - Always verify implementations against the target Nextcloud version
3. **Respect branching strategy** - Ensure changes are compatible with the target Nextcloud version
4. **Follow existing patterns** - The codebase has established patterns for controllers, services, and processors
5. **Test within Nextcloud** - Tests cannot run in isolation; they need the Nextcloud environment
5. **Integration tests need Nextcloud** - `tests/Integration/` and the full `tests/Unit/`
suite need the Nextcloud environment; most of `tests/Unit/` runs standalone via
`make php-unittest-standalone`
6. **Consider both backends** - Changes may affect both local CLI and external backend processing modes

### Before Committing Changes
Expand Down
39 changes: 36 additions & 3 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,40 @@ jobs:
php -S localhost:8080 &
- name: PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: make php-test
run: make php-integrationtest

# Runs tests/Unit without a Nextcloud checkout (see phpunit.standalone.xml). The
# sqlite/mysql/pgsql jobs above only run the integration suite now, since the unit
# suite is fully mocked and produces identical results regardless of DB backend. The
# full, NC-bootstrapped unit suite (phpunit.xml, including the files excluded here via
# the nextcloud-full group) still runs once, in the PHPUnit-Coverage workflow.
standalone:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
php-versions: ['8.4', '8.5']

name: standalone-php${{ matrix.php-versions }}

steps:
- name: Checkout app
uses: actions/checkout@v5

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

- name: Install dependencies
run: composer i

- name: PHPUnit (standalone)
run: make php-unittest-standalone

mysql:
runs-on: ubuntu-22.04
Expand Down Expand Up @@ -131,7 +164,7 @@ jobs:
php -S localhost:8080 &
- name: PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: make php-test
run: make php-integrationtest

pgsql:
runs-on: ubuntu-22.04
Expand Down Expand Up @@ -199,4 +232,4 @@ jobs:
php -S localhost:8080 &
- name: PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: make php-test
run: make php-integrationtest
42 changes: 36 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,30 @@ ExApp). Both repos are versioned in lock-step and share a REST contract — see

## Critical constraints

- **The app cannot run or be tested standalone.** PHP unit *and* integration tests boot
Nextcloud via `tests/bootstrap.php` and need a Nextcloud checkout with this app
installed under `apps/workflow_ocr` and enabled (`php occ app:enable workflow_ocr`).
Use the devcontainer (`.devcontainer/`) or mirror `.github/workflows/phpunit*.yml`.
JS tests (`vitest`) are the only suite that runs without Nextcloud.
- **Most of `tests/Unit/` runs standalone.** `make php-unittest-standalone`
(bootstrap `tests/bootstrap.standalone.php`, config `phpunit.standalone.xml`) needs
only `composer install` — no Nextcloud checkout — because it resolves `OCP\*`/`NCU\*`
via the `nextcloud/ocp` composer dependency instead of a real NC core, with a handful
of small polyfills in `tests/Unit/TestUtils/` for legacy/other-app classes
`nextcloud/ocp` doesn't ship (`OC\User\NoUserException`, `OC\Notification\Notification`,
`OCA\Files_Versions\Versions\*`, `OCA\AppAPI\PublicFunctions`). Seven files that touch a
real, non-trivial Nextcloud implementation (not just an interface) are excluded via
`#[Group('nextcloud-full')]` and still run only under `phpunit.xml`:
`AppInfo/ApplicationTest.php`, `OcrProcessors/OcrProcessorFactoryTest.php`,
`composer/AutoloadTest.php` (all construct a real `OCP\AppFramework\App`, needing
`\OC::$server` and Nextcloud's internal DI container), `Wrapper/ViewFactoryTest.php`
(real `OC\Files\View`), `BackgroundJobs/ProcessFileJobTest.php` and
`Listener/RegisterFlowOperationsListenerTest.php` (call real OCP methods that
internally use `OCP\Server::get()`/`\OC::$server`), and `Notification/NotifierTest.php`
(real `OC\Notification\Notification`, whose validation logic is too much to safely
polyfill). In CI, only the coverage workflow (`.github/workflows/sonarqube.yml`) still
runs the full, NC-bootstrapped unit suite; `phpunit.yml`'s other jobs use the
standalone suite instead.
- **`tests/Integration/` and the full `tests/Unit/` suite (`phpunit.xml`) still need a
full Nextcloud checkout.** Both boot Nextcloud via `tests/bootstrap.php` and need a
Nextcloud checkout with this app installed under `apps/workflow_ocr` and enabled
(`php occ app:enable workflow_ocr`). Use the devcontainer (`.devcontainer/`) or mirror
`.github/workflows/phpunit*.yml`. JS tests (`vitest`) also run without Nextcloud.
- **Every change must be considered against both backend modes** (local CLI and remote
ExApp). A change to settings, CLI arguments, or processors usually touches both paths.
- **Target Nextcloud version comes from `appinfo/info.xml`** (`<nextcloud min/max>`),
Expand All @@ -25,7 +44,9 @@ ExApp). Both repos are versioned in lock-step and share a REST contract — see

PHP 8.2–8.5 (composer platform pin 8.4, psalm `phpVersion` 8.2) · Vue 3 · Node ^24 /
npm ^11.6 · rsbuild + vitest · phpunit 12 · psalm 6.4 · php-cs-fixer via
`nextcloud/coding-standard` · `nextcloud/ocp` for OCP stubs.
`nextcloud/coding-standard` · `nextcloud/ocp` for OCP stubs and (as of the standalone
unit suite) real `OCP\*`/`NCU\*` symbols at test-run time — an unpinned `dev-master`
dependency, see "Critical constraints".

Namespace `OCA\WorkflowOcr\`, PSR-4 from `lib/`. Frontend sources in `src/`, built into
`js/` by rsbuild (`make npm-build`).
Expand Down Expand Up @@ -124,6 +145,7 @@ Run from the repo root. The Makefile is authoritative — there is no `make unit
```bash
make build # composer (no-dev) + npm install + rsbuild build
make php-unittest # phpunit -c phpunit.xml (tests/Unit)
make php-unittest-standalone # phpunit -c phpunit.standalone.xml (tests/Unit, no NC checkout)
make php-integrationtest # phpunit -c phpunit.integration.xml (tests/Integration)
make php-test # both PHP suites
make js-test # vitest unit + integration
Expand Down Expand Up @@ -152,6 +174,14 @@ clean. Never add to the psalm baseline to silence a new error you introduced.
matrix value names are load-bearing, do not rename them.
- Test PDFs/images live in `tests/Integration/testdata/`.
- JS tests are `src/test/**/*.spec.js` with `@nextcloud/*` mocks in `src/test/__mocks__/`.
- `tests/Unit/TestUtils/` holds standalone-suite-only scaffolding (an `invokePrivate`
replacement trait, plus polyfills for classes `nextcloud/ocp` doesn't ship) needed
only because `phpunit.standalone.xml` doesn't have a full Nextcloud core to draw on.
Mirrors the `tests/Integration/TestUtils/` convention. `nextcloud/ocp` itself ships no
composer `autoload` section, so `composer.json`'s `autoload-dev` adds a `classmap` over
`vendor/nextcloud/ocp/{OCP,NCU}/` to make its classes loadable at all, plus a PSR-4
mapping for `tests/Unit/` (so test-only support classes like
`tests/Unit/Service/IMetadataVersionWithBackend.php` autoload without a manual require).

## Conventions

Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ php-test: composer
php-unittest: composer
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.xml

.PHONY: php-unittest-standalone
php-unittest-standalone: composer
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.standalone.xml

.PHONY: php-integrationtest
php-integrationtest: composer
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml
Expand Down
9 changes: 9 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
"OCA\\WorkflowOcr\\": "lib/"
}
},
"autoload-dev": {
"psr-4": {
"OCA\\WorkflowOcr\\Tests\\Unit\\": "tests/Unit/"
},
"classmap": [
"vendor/nextcloud/ocp/OCP/",
"vendor/nextcloud/ocp/NCU/"
]
},
"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './node_modules/*' -exec php -l \"{}\" \\;",
"cs:check": "php-cs-fixer fix --dry-run --diff",
Expand Down
44 changes: 44 additions & 0 deletions phpunit.standalone.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.0/phpunit.xsd"
bootstrap="tests/bootstrap.standalone.php"
colors="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnPhpunitDeprecations="true">
<testsuites>
<testsuite name="unit-standalone">
<directory>./tests/Unit</directory>
<!-- Still extends Nextcloud's Test\TestCase, which doesn't exist here, so PHPUnit
would fatal just loading the file, before the group filtering below even
applies. Excluded by group too, for consistency with the other
nextcloud-full files. -->
<exclude>./tests/Unit/OcrProcessors/OcrProcessorFactoryTest.php</exclude>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>nextcloud-full</group>
</exclude>
</groups>
<source>
<include>
<directory suffix=".php">./</directory>
</include>
<exclude>
<directory suffix=".php">./appinfo</directory>
<directory suffix=".php">./l10n</directory>
<directory suffix=".php">./templates</directory>
<directory suffix=".php">./tests</directory>
<directory suffix=".php">./vendor</directory>
<directory suffix=".php">./node_modules</directory>
<directory suffix=".php">./lib/Migration</directory>
<directory suffix=".php">./lib/OcrProcessors/Remote/Client/Model</directory>
<file>./lib/OcrProcessors/Remote/Client/Configuration.php</file>
<file>./lib/OcrProcessors/Remote/Client/ObjectSerializer.php</file>
<file>./.php-cs-fixer.dist.php</file>
</exclude>
</source>
</phpunit>
5 changes: 5 additions & 0 deletions tests/Unit/AppInfo/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@

use OCA\WorkflowOcr\AppInfo\Application;
use OCP\AppFramework\Bootstrap\IBootContext;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

// Constructs a real OCP\AppFramework\App (via Application), which needs \OC::$server
// and Nextcloud's internal DI container — requires a full Nextcloud checkout. Excluded
// from the standalone suite (see phpunit.standalone.xml).
#[Group('nextcloud-full')]
class ApplicationTest extends TestCase {
public function testBootDoesNothingOnBootContext() {
/** @var IBootContext|MockObject */
Expand Down
5 changes: 5 additions & 0 deletions tests/Unit/BackgroundJobs/ProcessFileJobTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@
use OCA\WorkflowOcr\Service\IOcrService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

// Calling the real inherited start() (via QueuedJob/Job) internally calls
// OCP\Server::get(), which needs \OC::$server -- requires a full Nextcloud checkout.
// Excluded from the standalone suite (see phpunit.standalone.xml).
#[Group('nextcloud-full')]
class ProcessFileJobTest extends TestCase {
/** @var LoggerInterface|MockObject */
private $logger;
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Controller/GlobalSettingsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
use OCP\IRequest;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Test\TestCase;

class GlobalSettingsControllerTest extends TestCase {
/** @var IGlobalSettingsService|MockObject */
Expand Down
5 changes: 5 additions & 0 deletions tests/Unit/Listener/RegisterFlowOperationsListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@
use OCP\WorkflowEngine\Events\RegisterOperationsEvent;
use OCP\WorkflowEngine\IManager;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;

// Real OCP\Util::addScript() internally calls OCP\Server::get(), which needs
// \OC::$server -- requires a full Nextcloud checkout. Excluded from the standalone
// suite (see phpunit.standalone.xml).
#[Group('nextcloud-full')]
class RegisterFlowOperationsListenerTest extends TestCase {
/** @var ContainerInterface|MockObject */
private $container;
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Model/WorkflowSettingsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
use InvalidArgumentException;
use OCA\WorkflowOcr\Model\WorkflowSettings;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\TestCase;
use PHPUnit\Framework\TestCase;

class WorkflowSettingsTest extends TestCase {
#[DataProvider('dataProvider_testConstruction')]
Expand Down
6 changes: 6 additions & 0 deletions tests/Unit/Notification/NotifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,16 @@
use OCP\Notification\INotification;
use OCP\RichObjectStrings\IRichTextFormatter;
use OCP\RichObjectStrings\IValidator;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

// Instantiates the real Nextcloud\OC\Notification\Notification for its actual fluent
// setter/getter/validation behavior (not just as a mock target), which is too much
// internal logic to safely reproduce in a polyfill. Excluded from the standalone suite
// (see phpunit.standalone.xml).
#[Group('nextcloud-full')]
class NotifierTest extends TestCase {
/** @var IFactory|MockObject */
private $l10nFactory;
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/OcrProcessors/Local/ImageOcrProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
use OCA\WorkflowOcr\Wrapper\ICommand;
use OCP\Files\File;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Test\TestCase;

class ImageOcrProcessorTest extends TestCase {
/** @var \OCA\WorkflowOcr\Wrapper\IPhpNativeFunctions|MockObject */
Expand Down
6 changes: 6 additions & 0 deletions tests/Unit/OcrProcessors/OcrProcessorFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@
use OCA\WorkflowOcr\OcrProcessors\Remote\WorkflowOcrRemoteProcessor;
use OCA\WorkflowOcr\Service\IOcrBackendInfoService;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Container\ContainerInterface;
use Test\TestCase;

// Constructs a real OCP\AppFramework\App (via Application) and resolves services from
// its DI container, which needs \OC::$server and Nextcloud's internal DI container —
// requires a full Nextcloud checkout. Excluded from the standalone suite (see
// phpunit.standalone.xml).
#[Group('nextcloud-full')]
class OcrProcessorFactoryTest extends TestCase {
/** @var ContainerInterface */
private $appContainer;
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Service/GlobalSettingsServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
use OCA\WorkflowOcr\Service\GlobalSettingsService;
use OCP\IAppConfig;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
use PHPUnit\Framework\TestCase;

class GlobalSettingsServiceTest extends TestCase {
/** @var IAppConfig|MockObject */
Expand Down
5 changes: 4 additions & 1 deletion tests/Unit/Service/OcrServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
use OCA\WorkflowOcr\Service\IGlobalSettingsService;
use OCA\WorkflowOcr\Service\INotificationService;
use OCA\WorkflowOcr\Service\OcrService;
use OCA\WorkflowOcr\Tests\Unit\TestUtils\InvokesPrivateMethods;
use OCA\WorkflowOcr\Wrapper\IFilesystem;
use OCA\WorkflowOcr\Wrapper\IView;
use OCA\WorkflowOcr\Wrapper\IViewFactory;
Expand All @@ -57,10 +58,12 @@
use OCP\SystemTag\ISystemTagObjectMapper;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Test\TestCase;

class OcrServiceTest extends TestCase {
use InvokesPrivateMethods;

/** @var IOcrProcessorFactory|MockObject */
private $ocrProcessorFactory;
/** @var IOcrProcessor|MockObject */
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Settings/GlobalSettingsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
namespace OCA\WorkflowOcr\Tests\Unit\Settings;

use OCA\WorkflowOcr\Settings\GlobalSettings;
use Test\TestCase;
use PHPUnit\Framework\TestCase;

class GlobalSettingsTest extends TestCase {
/** @var GlobalSettings */
Expand Down
Loading
Loading