Testing
OpenRegister uses comprehensive integration tests to ensure functionality works correctly across all components.
Test Strategy
Integration Testing
Integration tests verify that different parts of the system work together correctly by testing against a running Docker environment with:
- Nextcloud container
- MySQL database
- Real HTTP requests
- Actual file storage
Location
Integration tests are located in:
openregister/tests/Integration/
Core Integration Tests
The CoreIntegrationTest.php file contains organized test groups covering all major OpenRegister functionality.
Test Group 1: File Upload Tests (Tests 1-15)
Tests for file attachment and upload functionality.
Covered Functionality
Single File Uploads
- Multipart form upload
- Base64 data URI upload
- URL reference upload (external files)
Multiple File Uploads
- Multiple files in single request
- File arrays (images[])
Validation
- MIME type validation
- File size limits
- Corrupted base64 detection
File Operations
- Retrieving file metadata
- Updating files
- Mixed upload methods
Example Test
public function testMultipartUploadSinglePdf(): void
{
$pdfContent = '%PDF-1.4 fake pdf content for testing';
$tmpFile = tmpfile();
fwrite($tmpFile, $pdfContent);
$response = $this->client->post(
"/index.php/apps/openregister/api/objects/{$register}/{$schema}",
[
'multipart' => [
['name' => 'title', 'contents' => 'Test Document'],
['name' => 'attachment', 'contents' => fopen($tmpPath, 'r'),
'filename' => 'test.pdf',
'headers' => ['Content-Type' => 'application/pdf']
],
]
]
);
$this->assertEquals(201, $response->getStatusCode());
}
Test Group 2: Cascade Protection Tests (Tests 16-18)
Tests for referential integrity and cascade protection.
Covered Functionality
- Register Protection: Cannot delete register with objects
- Schema Protection: Cannot delete schema with objects
- Cleanup Workflow: Can delete after proper cleanup
Why This Matters
Cascade protection prevents accidental data loss by ensuring:
- Registers cannot be deleted while containing schemas or objects
- Schemas cannot be deleted while containing objects
- Proper cleanup order is enforced (objects → schemas → registers)
Example Test
public function testCannotDeleteRegisterWithObjects(): void
{
// Create object
$response = $this->client->post(
"/index.php/apps/openregister/api/objects/{$register}/{$schema}",
['json' => ['title' => 'Protection Test']]
);
// Attempt to delete register
$deleteResponse = $this->client->delete(
"/index.php/apps/openregister/api/registers/{$registerId}"
);
// Should fail with 400 or 409
$this->assertContains($deleteResponse->getStatusCode(), [400, 409]);
}
Test Group 3: File Publishing Tests (Tests 19-22)
Tests for file sharing, publishing, and metadata.
Covered Functionality
File Access Control
- Authenticated URLs for non-shared files (
/api/files/) - Public share URLs for published files (
/index.php/s/)
Auto-Publishing
- Schema-level
autoPublishconfiguration - Automatic share creation on upload
Metadata Integration
- Logo field mapping (
objectImageField) - Image metadata in
@self.image - First-file-in-array selection
File Deletion
- Delete single file by sending
null - Delete file array by sending
[]
Example Test
public function testAutoShareFileProperty(): void
{
// Create schema with autoPublish
$schemaResponse = $this->client->post(
'/index.php/apps/openregister/api/schemas',
[
'json' => [
'register' => $registerId,
'slug' => 'auto-share-test',
'properties' => [
'document' => [
'type' => 'file',
'autoPublish' => true
],
],
]
]
);
// Upload file
$response = $this->client->post(
"/index.php/apps/openregister/api/objects/{$register}/{$schema}",
['multipart' => [...]]
);
$object = json_decode($response->getBody(), true);
// Verify public share URL
$this->assertArrayHasKey('published', $object['document']);
$this->assertStringContainsString(
'/index.php/s/',
$object['document']['accessUrl']
);
}
Test Group 4: Array Filtering Tests (Tests 23-30)
Tests for advanced filtering with AND/OR logic and dot notation.
What These Tests Cover
These tests verify filtering functionality across multiple registers and schemas, as well as array properties within objects:
Cross-Register/Schema Filtering
- Test objects created in different registers (Register 1, Register 2)
- Test objects created with different schemas (Schema 1, Schema 2)
- Verify AND logic returns zero results when filtering single-value fields for multiple values
- Verify OR logic returns objects from multiple registers/schemas
Object Array Property Filtering
- Test objects with array properties (e.g., 'availableColours': ['red', 'blue'])
- Verify AND logic requires ALL values present in the array
- Verify OR logic matches objects with ANY of the specified values
Covered Functionality
Default AND Logic
- Metadata arrays:
@self.register[]=1&@self.register[]=2→ zero results (object can't be in BOTH registers) - Object arrays:
colours[]=red&colours[]=blue→ objects with BOTH colors in their array
Explicit OR Logic
- Metadata:
@self.register[or]=1,2→ objects from EITHER register 1 OR register 2 - Objects:
colours[or]=red,blue→ objects with EITHER red OR blue (or both)
Dot Notation Syntax
- Clean URLs:
@self.fieldinstead of@self[field] - Works with operators:
@self.created[gte]=2025-01-01 - Combines with regular filters:
@self.register=5&title=Test
Complex Scenarios
- Multiple filter types combined
- Nested operators
- Mixed AND/OR logic across different fields
Example Tests
Default AND Logic - Cross-Register Testing
public function testMetadataArrayFilterDefaultAndLogic(): void
{
// Step 1: Create TWO separate registers
$register1 = $this->client->post('/api/registers', [
'json' => ['slug' => 'filter-test-1', 'title' => 'Filter Test Register 1']
]);
$register2 = $this->client->post('/api/registers', [
'json' => ['slug' => 'filter-test-2', 'title' => 'Filter Test Register 2']
]);
// Step 2: Create schemas in EACH register
$schema1 = createSchemaInRegister($register1['id']);
$schema2 = createSchemaInRegister($register2['id']);
// Step 3: Create objects in DIFFERENT registers
$obj1 = createObjectInRegister($register1, $schema1); // In Register 1
$obj2 = createObjectInRegister($register2, $schema2); // In Register 2
// Step 4: Filter with AND logic (default) - search across ALL registers
$url = "/api/objects?@self.register[]={$reg1['id']}&@self.register[]={$reg2['id']}";
$response = $this->client->get($url);
$result = json_decode($response->getBody(), true);
// Step 5: Verify zero results (object can't be in BOTH registers simultaneously)
$this->assertEquals(0, $result['total']);
}
Explicit OR Logic - Cross-Register Testing
public function testMetadataArrayFilterExplicitOrLogicWithDotNotation(): void
{
// Step 1: Create TWO separate registers
$register1 = createRegister('or-test-1');
$register2 = createRegister('or-test-2');
// Step 2: Create schemas in EACH register
$schema1 = createSchemaInRegister($register1['id']);
$schema2 = createSchemaInRegister($register2['id']);
// Step 3: Create objects in DIFFERENT registers
$obj1 = createObjectInRegister($register1, $schema1); // In Register 1
$obj2 = createObjectInRegister($register2, $schema2); // In Register 2
// Step 4: Filter with OR logic using dot notation - search across ALL registers
$url = "/api/objects?@self.register[or]={$reg1['id']},{$reg2['id']}";
$response = $this->client->get($url);
$result = json_decode($response->getBody(), true);
// Step 5: Verify BOTH objects returned (from register 1 OR register 2)
$this->assertGreaterThanOrEqual(2, $result['total']);
$returnedIds = array_column($result['results'], 'id');
$this->assertContains($obj1['id'], $returnedIds); // From Register 1
$this->assertContains($obj2['id'], $returnedIds); // From Register 2
}
Object Array Property AND Logic - Within Same Register/Schema
public function testObjectArrayPropertyDefaultAndLogic(): void
{
// Step 1: Create ONE register with ONE schema that has array property
$register = createRegister('product-test');
$schema = $this->client->post('/api/schemas', [
'json' => [
'register' => $register['id'],
'slug' => 'product-schema',
'properties' => [
'title' => ['type' => 'string'],
'availableColours' => [
'type' => 'array',
'items' => ['type' => 'string']
]
],
]
]);
// Step 2: Create products with different color combinations IN SAME REGISTER
$redBlue = createProduct(['red', 'blue']); // ✅ Has BOTH red AND blue
$onlyBlue = createProduct(['blue']); // ❌ Has only blue, missing red
$redBlueGreen = createProduct(['red', 'blue', 'green']); // ✅ Has BOTH red AND blue (plus green)
// Step 3: Filter for products with BOTH red AND blue using AND logic
$url = "/api/objects?availableColours[]=red&availableColours[]=blue";
$response = $this->client->get($url);
$result = json_decode($response->getBody(), true);
// Step 4: Verify only products with BOTH colors are returned
$returnedIds = array_column($result['results'], 'id');
$this->assertContains($redBlue['id'], $returnedIds); // Has both
$this->assertContains($redBlueGreen['id'], $returnedIds); // Has both + more
$this->assertNotContains($onlyBlue['id'], $returnedIds); // Missing red
}
Dot Notation Syntax
public function testDotNotationSyntaxForMetadataFilters(): void
{
// Use dot notation for metadata filter
$url = "/api/objects?@self.register={$registerId}";
$response = $this->client->get($url);
$result = json_decode($response->getBody(), true);
// All returned objects should be from specified register
foreach ($result['results'] as $obj) {
$this->assertEquals($registerId, $obj['@self']['register']);
}
}
Running Tests
Prerequisites
-
Docker containers running:
docker ps | grep -E "nextcloud|database" -
OpenRegister app enabled:
docker exec -u 33 master-nextcloud-1 php occ app:enable openregister
Run Tests Inside Docker Container (Recommended)
Integration tests should be run inside the Nextcloud Docker container to have access to the full Nextcloud environment:
# Run all tests
docker exec master-nextcloud-1 bash -c "cd /var/www/html/apps-extra/openregister && php vendor/bin/phpunit tests/Integration/CoreIntegrationTest.php --bootstrap tests/integration-bootstrap.php --no-configuration 2>&1"
# Run specific test group with readable output
docker exec master-nextcloud-1 bash -c "cd /var/www/html/apps-extra/openregister && php vendor/bin/phpunit --filter 'testMetadataArrayFilter|testObjectArrayProperty' tests/Integration/CoreIntegrationTest.php --bootstrap tests/integration-bootstrap.php --no-configuration --testdox 2>&1"
# Run single test
docker exec master-nextcloud-1 bash -c "cd /var/www/html/apps-extra/openregister && php vendor/bin/phpunit --filter testDotNotationSyntaxForMetadataFilters tests/Integration/CoreIntegrationTest.php --bootstrap tests/integration-bootstrap.php --no-configuration 2>&1"
# Save output to file
docker exec master-nextcloud-1 bash -c "cd /var/www/html/apps-extra/openregister && php vendor/bin/phpunit tests/Integration/CoreIntegrationTest.php --bootstrap tests/integration-bootstrap.php --no-configuration 2>&1 | tee /tmp/test-output.txt"
Run Specific Test Groups
# File Upload Tests (1-15)
docker exec master-nextcloud-1 bash -c "cd /var/www/html/apps-extra/openregister && php vendor/bin/phpunit --filter testMultipart tests/Integration/CoreIntegrationTest.php --bootstrap tests/integration-bootstrap.php --no-configuration --testdox"
# Cascade Protection Tests (16-18)
docker exec master-nextcloud-1 bash -c "cd /var/www/html/apps-extra/openregister && php vendor/bin/phpunit --filter testCannotDelete tests/Integration/CoreIntegrationTest.php --bootstrap tests/integration-bootstrap.php --no-configuration --testdox"
# File Publishing Tests (19-22)
docker exec master-nextcloud-1 bash -c "cd /var/www/html/apps-extra/openregister && php vendor/bin/phpunit --filter 'testAutoShare|testLogo|testImage|testDelete' tests/Integration/CoreIntegrationTest.php --bootstrap tests/integration-bootstrap.php --no-configuration --testdox"
# Array Filtering Tests (23-30)
docker exec master-nextcloud-1 bash -c "cd /var/www/html/apps-extra/openregister && php vendor/bin/phpunit --filter 'testMetadataArrayFilter|testObjectArrayProperty|testDotNotation|testComplex' tests/Integration/CoreIntegrationTest.php --bootstrap tests/integration-bootstrap.php --no-configuration --testdox"
Why Run in Docker Container?
Running tests inside the Docker container ensures:
- Access to Nextcloud's internal API
- Proper database connections
- File system access
- Nextcloud environment variables
- PHP extensions and dependencies
- Correct base URL ('http://localhost' inside container)
Test Environment
Configuration
Tests use the following configuration:
- Base URL:
http://localhost - Auth:
admin:admin(Basic Auth) - Containers:
master-nextcloud-1(Nextcloud)master-database-mysql-1(MySQL)
Cleanup
Tests automatically clean up created resources:
- Objects (deleted first)
- Schemas (deleted second)
- Registers (deleted last)
Proper cleanup order is essential for cascade protection.
Writing New Tests
Test Structure
public function testYourFeature(): void
{
// 1. Setup: Create necessary resources
$register = $this->createTestRegister();
$schema = $this->createTestSchema($register);
// 2. Execute: Perform the test action
$response = $this->client->post(
"/api/objects/{$register}/{$schema}",
['json' => ['title' => 'Test']]
);
// 3. Assert: Verify expected behavior
$this->assertEquals(201, $response->getStatusCode());
$data = json_decode($response->getBody(), true);
$this->assertArrayHasKey('id', $data);
// 4. Cleanup: Track for tearDown
$this->createdObjectIds[] = $data['id'];
}
Best Practices
- Use unique identifiers: Add
uniqid()to slugs to avoid conflicts - Track created resources: Add IDs to cleanup arrays
- Clear assertions: Use descriptive assertion messages
- Isolated tests: Each test should be independent
- Cleanup order: Objects → Schemas → Registers
Adding to Test Groups
When adding tests, follow the existing group structure:
- Tests 1-15: File uploads
- Tests 16-18: Cascade protection
- Tests 19-22: File publishing
- Tests 23-30: Array filtering
- Tests 31+: Your new group
Update the class-level docblock when adding new groups.
Continuous Integration
GitHub Actions
Tests can be integrated with GitHub Actions:
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Start Docker containers
run: docker-compose up -d
- name: Run tests
run: ./vendor/bin/phpunit tests/Integration/
Troubleshooting
Container Connection Issues
# Check container status
docker ps
# Check container logs
docker logs master-nextcloud-1
# Restart containers
docker-compose restart
Database Issues
# Access database
docker exec -it master-database-mysql-1 mysql -u nextcloud -pnextcloud nextcloud
# Check object count
SELECT COUNT(*) FROM oc_openregister_objects;
# Clean test data
DELETE FROM oc_openregister_objects WHERE register IN (
SELECT id FROM oc_openregister_registers WHERE slug LIKE 'test-%'
);
Test Failures
- Check container logs for errors
- Verify app is enabled:
php occ app:list | grep openregister - Check database connections
- Ensure proper cleanup between test runs
Test Scope: Cross-Register vs Within-Register
Understanding test scope is important for debugging and extending tests:
Cross-Register/Schema Tests (Tests 21-22, 25-27)
These tests create multiple registers and schemas to verify filtering works across different data boundaries:
graph TB
subgraph "Register 1"
S1[Schema 1]
O1[Object 1]
S1 --> O1
end
subgraph "Register 2"
S2[Schema 2]
O2[Object 2]
S2 --> O2
end
API[API Filter Query]
API -->|"@self.register[or]=1,2"| O1
API -->|"@self.register[or]=1,2"| O2
style API fill:#f9f,stroke:#333
style O1 fill:#bfb,stroke:#333
style O2 fill:#bfb,stroke:#333
Purpose: Verify that:
- OR logic can retrieve objects from multiple registers
- AND logic correctly returns zero results for single-value fields
- Dot notation works across register boundaries
Within-Register Tests (Tests 23-24, 28)
These tests use one register with one schema to verify array property filtering:
graph TB
subgraph "Single Register"
S1[Product Schema]
O1["Object 1<br/>colors: [red, blue]"]
O2["Object 2<br/>colors: [blue]"]
O3["Object 3<br/>colors: [red, blue, green]"]
S1 --> O1
S1 --> O2
S1 --> O3
end
API[API Filter Query]
API -->|"colors[]=red&colors[]=blue"| O1
API -->|"colors[]=red&colors[]=blue"| O3
style API fill:#f9f,stroke:#333
style O1 fill:#bfb,stroke:#333
style O3 fill:#bfb,stroke:#333
style O2 fill:#fbb,stroke:#333
Purpose: Verify that:
- AND logic requires ALL array values to be present
- OR logic matches ANY array value
- Array filtering works within object properties
Future Test Groups
Planned additional test groups:
- RBAC Tests: Role-based access control
- Multi-tenancy Tests: Organization isolation
- Search Tests: Full-text and filtered search
- Solr Integration Tests: Search engine functionality
- Performance Tests: Bulk operations and optimization
See Also
- Search Documentation - Array filtering details
- File Management - File upload specifications
- API Documentation - API endpoint reference