Files
appwrite/tests/e2e/Services/Databases/Transactions/ACIDBase.php
Chirag Aggarwal d2230f8fe7 chore: bump PHPStan to level 4 and fix all new errors
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:

- `app/controllers/general.php`: path-traversal guard was negating
  `\substr(...)` before the strict comparison (`!\substr(...) === $base`
  was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
  and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
  `DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
  result as an array with `deviceName/deviceBrand/deviceModel` keys.
  Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
  used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
  key was checking `$resourceKey === 'functions'` when `$resourceKey`
  is `'functionId'|'siteId'` — always false. Switched to the intended
  `$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
  to `string|false` to match `openssl_encrypt`; this lets callers'
  `=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
  `array_key_exists('from', [])` branch in the Msg91 provider (empty
  array literal; branch was unreachable).

Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
  PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
  `markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
  on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
  on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
  were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
  Installer State, `$source` on MigrationsWorker, `$account2` on two
  GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
  and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
  `assertNotNull` assertions with `addToAssertionCount(1)` or
  `assertNotEmpty` where the runtime type was already known.
2026-04-19 17:31:20 +05:30

634 lines
27 KiB
PHP

<?php
namespace Tests\E2E\Services\Databases\Transactions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\SchemaPolling;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
trait ACIDBase
{
use SchemaPolling;
/**
* Test atomicity - all operations succeed or all fail
*/
public function testAtomicity(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'AtomicityTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection with unique constraint
$collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
$this->getContainerIdParam() => ID::unique(),
'name' => 'AtomicityTest',
$this->getSecurityParam() => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
if ($this->getSupportForAttributes()) {
// Add unique attribute
$this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'email',
'size' => 256,
'required' => true,
]);
$this->waitForAllAttributes($databaseId, $collectionId);
}
// Add unique index
$this->client->call(Client::METHOD_POST, $this->getIndexUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'unique_email',
'type' => Database::INDEX_UNIQUE,
$this->getIndexAttributesParam() => ['email']
]);
$this->waitForIndex($databaseId, $collectionId, 'unique_email');
// Create first document outside transaction
$doc1 = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
$this->getRecordIdParam() => ID::unique(),
'data' => [
'email' => 'existing@example.com'
]
]);
$this->assertEquals(201, $doc1['headers']['status-code']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed. Response: ' . json_encode($transaction));
$this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id. Response body: ' . json_encode($transaction['body']));
$transactionId = $transaction['body']['$id'];
// Add operations - second one will fail due to unique constraint
$response = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId) . "/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'create',
$this->getRecordIdParam() => ID::unique(),
'data' => [
'email' => 'newuser@example.com' // This should succeed
]
],
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'create',
$this->getRecordIdParam() => ID::unique(),
'data' => [
'email' => 'existing@example.com' // This will fail - duplicate
]
],
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'create',
$this->getRecordIdParam() => ID::unique(),
'data' => [
'email' => 'anotheruser@example.com' // This should not be created due to atomicity
]
]
]
]);
$this->assertEquals(201, $response['headers']['status-code'], 'Add operations failed. Response: ' . json_encode($response['body']));
// Attempt to commit - should fail due to unique constraint violation
$response = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
if ($response['headers']['status-code'] === 200) {
// If transaction succeeded, all documents should be created
$documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// Should have 4 documents total (1 original + 3 from transaction)
// But since we have a unique constraint violation, this might fail
$this->assertGreaterThanOrEqual(1, $documents['body']['total']);
} else {
$this->assertEquals(409, $response['headers']['status-code']); // Conflict error
// Verify NO new documents were created (atomicity)
$documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(1, $documents['body']['total']); // Only the original document
$this->assertEquals('existing@example.com', $documents['body'][$this->getRecordResource()][0]['email']);
}
}
/**
* Test consistency - schema validation and constraints
*/
public function testConsistency(): void
{
if (!$this->getSupportForAttributes()) {
$this->markTestSkipped('This adapter does not support attributes; schema constraint consistency cannot be tested.');
}
// Create database
$database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'ConsistencyTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection with required fields and constraints
$collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
$this->getContainerIdParam() => ID::unique(),
'name' => 'ConsistencyTest',
$this->getSecurityParam() => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
// Add required string attribute
$this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'required_field',
'size' => 256,
'required' => true,
]);
// Add integer attribute with min/max constraints
$this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'integer'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'age',
'required' => true,
'min' => 18,
'max' => 100
]);
$this->waitForAllAttributes($databaseId, $collectionId);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$transactionId = $transaction['body']['$id'];
// Add operations with both valid and invalid data
$response = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId) . "/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'create',
$this->getRecordIdParam() => ID::unique(),
'data' => [
'required_field' => 'Valid User',
'age' => 25 // Valid age
]
],
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'create',
$this->getRecordIdParam() => ID::unique(),
'data' => [
'required_field' => 'Too Young User',
'age' => 10 // Below minimum - will fail constraint
]
],
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'create',
$this->getRecordIdParam() => ID::unique(),
'data' => [
'required_field' => 'Another Valid User',
'age' => 30 // Valid but should not be created due to transaction failure
]
]
]
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Attempt to commit - should fail due to constraint violation
$response = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(400, $response['headers']['status-code'], 'Transaction commit should fail with 400 due to constraint violation. Response: ' . json_encode($response['body']));
// Verify no documents were created
$documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(0, $documents['body']['total']);
}
/**
* Test isolation - concurrent transactions on same data
*/
public function testIsolation(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'IsolationTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
$this->getContainerIdParam() => ID::unique(),
'name' => 'IsolationTest',
$this->getSecurityParam() => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
if ($this->getSupportForAttributes()) {
// Add counter attribute
$this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'integer'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'counter',
'required' => true,
'min' => 0,
'max' => 1000000
]);
$this->waitForAllAttributes($databaseId, $collectionId);
}
// Create initial document with counter
$doc = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
$this->getRecordIdParam() => 'shared_counter',
'data' => [
'counter' => 0
]
]);
$this->assertEquals(201, $doc['headers']['status-code']);
// Create first transaction
$transaction1 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction1['headers']['status-code'], 'Transaction 1 creation should succeed');
$this->assertArrayHasKey('$id', $transaction1['body'], 'Transaction 1 response should have $id');
$transactionId1 = $transaction1['body']['$id'];
// Create second transaction
$transaction2 = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction2['headers']['status-code'], 'Transaction 2 creation should succeed');
$this->assertArrayHasKey('$id', $transaction2['body'], 'Transaction 2 response should have $id');
$transactionId2 = $transaction2['body']['$id'];
// Transaction 1: Increment counter by 10
$this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId1) . "/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
$this->getRecordIdParam() => 'shared_counter',
'action' => 'increment',
'data' => [
$this->getSchemaParam() => 'counter',
'value' => 10
]
]
]
]);
// Transaction 2: Increment counter by 5
$this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId2) . "/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
$this->getRecordIdParam() => 'shared_counter',
'action' => 'increment',
'data' => [
$this->getSchemaParam() => 'counter',
'value' => 5
]
]
]
]);
// Commit first transaction
$response1 = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId1), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response1['headers']['status-code']);
// Commit second transaction
$response2 = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId2), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response2['headers']['status-code']);
// Check final value - both increments should be applied
$document = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, 'shared_counter'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// Both increments should be applied: 0 + 10 + 5 = 15
$this->assertEquals(15, $document['body']['counter']);
}
/**
* Test durability - committed data persists
*/
public function testDurability(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, $this->getDatabaseUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'DurabilityTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $this->client->call(Client::METHOD_POST, $this->getContainerUrl($databaseId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
$this->getContainerIdParam() => ID::unique(),
'name' => 'DurabilityTest',
$this->getSecurityParam() => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
if ($this->getSupportForAttributes()) {
// Add attribute
$this->client->call(Client::METHOD_POST, $this->getSchemaUrl($databaseId, $collectionId, 'string'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'data',
'size' => 256,
'required' => true,
]);
$this->waitForAllAttributes($databaseId, $collectionId);
}
// Create and commit transaction with multiple operations
$transaction = $this->client->call(Client::METHOD_POST, $this->getTransactionUrl(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed');
$this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id');
$transactionId = $transaction['body']['$id'];
// Add multiple operations
$this->client->call(Client::METHOD_POST, $this->getTransactionUrl($transactionId) . "/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'create',
$this->getRecordIdParam() => 'durable_doc_1',
'data' => [
'data' => 'Important data 1'
]
],
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'create',
$this->getRecordIdParam() => 'durable_doc_2',
'data' => [
'data' => 'Important data 2'
]
],
[
'databaseId' => $databaseId,
$this->getContainerIdParam() => $collectionId,
'action' => 'update',
$this->getRecordIdParam() => 'durable_doc_1',
'data' => [
'data' => 'Updated important data 1'
]
]
]
]);
// Commit transaction
$response = $this->client->call(Client::METHOD_PATCH, $this->getTransactionUrl($transactionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response['headers']['status-code'], 'Commit should succeed. Response: ' . json_encode($response['body']));
$this->assertEquals('committed', $response['body']['status']);
// List all documents to see what was created
$allDocs = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertGreaterThan(0, $allDocs['body']['total'], 'Should have created documents. Found: ' . json_encode($allDocs['body']));
// Verify documents exist and have correct data
$document1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, 'durable_doc_1'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $document1['headers']['status-code']);
$this->assertEquals('Updated important data 1', $document1['body']['data']);
$document2 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, 'durable_doc_2'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $document2['headers']['status-code']);
$this->assertEquals('Important data 2', $document2['body']['data']);
// Further update outside transaction to ensure persistence
$update = $this->client->call(Client::METHOD_PATCH, $this->getRecordUrl($databaseId, $collectionId, 'durable_doc_1'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'data' => 'Modified outside transaction'
]
]);
$this->assertEquals(200, $update['headers']['status-code']);
// Verify the update persisted
$document1 = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId, 'durable_doc_1'), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals('Modified outside transaction', $document1['body']['data']);
// List all documents to verify total count
$documents = $this->client->call(Client::METHOD_GET, $this->getRecordUrl($databaseId, $collectionId), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(2, $documents['body']['total']);
}
}