From 33383ff1191490ae57b7dc0195d47330fad177ac Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 17 May 2026 03:10:24 +0000 Subject: [PATCH 1/3] chore: drop location column and handle N-part resource paths Removes the unused `location` column from the audit schema. The cloud audit pipeline resolves country at write time instead, and no consumer relies on the legacy `location` field. Generalizes `SQL::parseResource()` to handle any even number of segments shaped as alternating `/`. Previously only 2-part and 4-part paths were parsed; 6-part paths like `database//collection//document/` fell through to the default fallback, leaving `resourceType` and `resourceParent` empty. Breaking change: `Audit::log()` no longer accepts the `$location` positional parameter; the `location` key is removed from the create / createBatch log shape and from `Log::getLocation()`. Co-Authored-By: Claude Opus 4.7 --- src/Audit/Adapter.php | 2 -- src/Audit/Adapter/SQL.php | 36 +++++++++-------------- src/Audit/Audit.php | 8 ++---- src/Audit/Log.php | 11 ------- tests/Audit/Adapter/ClickHouseTest.php | 30 ++++++++++++++++--- tests/Audit/AuditBase.php | 40 +++++++------------------- 6 files changed, 54 insertions(+), 73 deletions(-) diff --git a/src/Audit/Adapter.php b/src/Audit/Adapter.php index a6d5a53..33f0d8b 100644 --- a/src/Audit/Adapter.php +++ b/src/Audit/Adapter.php @@ -43,7 +43,6 @@ abstract public function getById(string $id): ?Log; * resource: string, * userAgent: string, * ip: string, - * location?: string, * data?: array * } $log * @return Log The created log entry @@ -61,7 +60,6 @@ abstract public function create(array $log): Log; * resource: string, * userAgent: string, * ip: string, - * location?: string, * time: \DateTime|string|null, * data?: array * }> $logs diff --git a/src/Audit/Adapter/SQL.php b/src/Audit/Adapter/SQL.php index f35625b..4307db4 100644 --- a/src/Audit/Adapter/SQL.php +++ b/src/Audit/Adapter/SQL.php @@ -88,15 +88,6 @@ public function getAttributes(): array 'array' => false, 'filters' => [], ], - [ - '$id' => 'location', - 'type' => Database::VAR_STRING, - 'size' => 45, - 'required' => false, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], [ '$id' => 'time', 'type' => Database::VAR_DATETIME, @@ -222,30 +213,31 @@ protected function getAllColumnDefinitions(): array /** * Parses the resource string from the payload and extracts its ID, type, and parent. * + * Supports any even number of segments shaped as alternating `/`, + * e.g. `database/`, `database//collection/`, + * `database//collection//document/`. The last segment is the + * resource id, the second-to-last is the resource type, and any preceding + * segments form the resource parent path. + * * @param string $resource * @return array{ resourceId: string, resourceType: string, resourceParent: string } */ protected function parseResource(string $resource): array { $parts = explode('/', $resource); + $count = count($parts); + $resourceId = $resource; $resourceType = ''; $resourceParent = ''; - // resource/resourceId/subResource/subResourceId - if (count($parts) === 4) { - $resourceId = $parts[3]; - $resourceType = $parts[2]; + if ($count >= 2 && $count % 2 === 0) { + $resourceId = $parts[$count - 1]; + $resourceType = $parts[$count - 2]; - // resource/resourceId - $resourceParent = "{$parts[0]}/{$parts[1]}"; - } // resource/resourceId - elseif (count($parts) === 2) { - $resourceId = $parts[1]; - $resourceType = $parts[0]; - } else { - // default fallback - $resourceId = $resource; + if ($count > 2) { + $resourceParent = implode('/', array_slice($parts, 0, $count - 2)); + } } return [ diff --git a/src/Audit/Audit.php b/src/Audit/Audit.php index 0e1deb4..bd51560 100644 --- a/src/Audit/Audit.php +++ b/src/Audit/Audit.php @@ -52,22 +52,20 @@ public function setup(): void * @param string $resource * @param string $userAgent * @param string $ip - * @param string $location * @param array $data * @return Log * * @throws \Exception */ - public function log(?string $userId, string $event, string $resource, string $userAgent, string $ip, string $location, array $data = []): Log + public function log(?string $userId, string $event, string $resource, string $userAgent, string $ip, array $data = []): Log { - /** @var array{userId?: string|null, event: string, resource: string, userAgent: string, ip: string, location?: string, data?: array} $log */ + /** @var array{userId?: string|null, event: string, resource: string, userAgent: string, ip: string, data?: array} $log */ $log = [ 'userId' => $userId, 'event' => $event, 'resource' => $resource, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => $data, ]; @@ -77,7 +75,7 @@ public function log(?string $userId, string $event, string $resource, string $us /** * Add multiple event logs in batch. * - * @param array}> $events + * @param array}> $events * @return bool * * @throws \Exception diff --git a/src/Audit/Log.php b/src/Audit/Log.php index ffbf197..f251575 100644 --- a/src/Audit/Log.php +++ b/src/Audit/Log.php @@ -90,17 +90,6 @@ public function getIp(): string return is_string($ip) ? $ip : ''; } - /** - * Get the location information. - * - * @return string|null - */ - public function getLocation(): ?string - { - $location = $this->getAttribute('location'); - return is_string($location) ? $location : null; - } - /** * Get the timestamp. * diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index b50da71..07b138c 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -418,7 +418,6 @@ public function testBatchOperationsWithSpecialCharacters(): void 'resource' => 'doc/"quotes"', 'userAgent' => "User'Agent\"With'Quotes", 'ip' => '192.168.1.1', - 'location' => 'UK', 'data' => ['special' => "data with 'quotes'"], 'time' => \Utopia\Database\DateTime::formatTz(\Utopia\Database\DateTime::now()) ?? '' ] @@ -519,7 +518,6 @@ public function testParseResourceComplexPath(): void $userId = 'parseUser'; $userAgent = 'UnitTestAgent/1.0'; $ip = '127.0.0.1'; - $location = 'US'; $resource = 'database/6978484940ff05762e1a/table/697848498066e3d2ef64'; @@ -532,7 +530,7 @@ public function testParseResourceComplexPath(): void unset($required['resourceType'], $required['resourceId'], $required['resourceParent']); $dataWithAttributes = array_merge($data, $required); - $log = $this->audit->log($userId, 'create', $resource, $userAgent, $ip, $location, $dataWithAttributes); + $log = $this->audit->log($userId, 'create', $resource, $userAgent, $ip, $dataWithAttributes); $this->assertInstanceOf(\Utopia\Audit\Log::class, $log); @@ -566,6 +564,30 @@ public function testParseResourceMethod(): void $this->assertEquals('697848498066e3d2ef64', $parsed['resourceId']); $this->assertEquals('table', $parsed['resourceType']); $this->assertEquals('database/6978484940ff05762e1a', $parsed['resourceParent']); + + $sixPart = 'database/693586330029ae2f0d3f/collection/watch_history/document/6a06d9a7001c3cd05d20'; + /** @var array{resourceId: string, resourceType: string, resourceParent: string} $parsedSix */ + $parsedSix = $method->invoke($adapter, $sixPart); + + $this->assertEquals('6a06d9a7001c3cd05d20', $parsedSix['resourceId']); + $this->assertEquals('document', $parsedSix['resourceType']); + $this->assertEquals('database/693586330029ae2f0d3f/collection/watch_history', $parsedSix['resourceParent']); + + $twoPart = 'user/abc123'; + /** @var array{resourceId: string, resourceType: string, resourceParent: string} $parsedTwo */ + $parsedTwo = $method->invoke($adapter, $twoPart); + + $this->assertEquals('abc123', $parsedTwo['resourceId']); + $this->assertEquals('user', $parsedTwo['resourceType']); + $this->assertEquals('', $parsedTwo['resourceParent']); + + $oddPart = 'foo/bar/baz'; + /** @var array{resourceId: string, resourceType: string, resourceParent: string} $parsedOdd */ + $parsedOdd = $method->invoke($adapter, $oddPart); + + $this->assertEquals('foo/bar/baz', $parsedOdd['resourceId']); + $this->assertEquals('', $parsedOdd['resourceType']); + $this->assertEquals('', $parsedOdd['resourceParent']); } public function testCursorAfterPaginatesLogs(): void @@ -813,7 +835,7 @@ public function testSelectAutoIncludesTenantWhenShared(): void $adapter->setup(); $audit = new Audit($adapter); - $audit->log('u1', 'create', 'doc/1', 'agent', '127.0.0.1', 'US', $this->getRequiredAttributes()); + $audit->log('u1', 'create', 'doc/1', 'agent', '127.0.0.1', $this->getRequiredAttributes()); $logs = $audit->find([ Query::select(['event']), diff --git a/tests/Audit/AuditBase.php b/tests/Audit/AuditBase.php index 823f4a5..30531b1 100644 --- a/tests/Audit/AuditBase.php +++ b/tests/Audit/AuditBase.php @@ -51,16 +51,15 @@ public function createLogs(): void $userId = 'userId'; $userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'; $ip = '127.0.0.1'; - $location = 'US'; $data = ['key1' => 'value1', 'key2' => 'value2']; $requiredAttributes = $this->getRequiredAttributes(); $dataWithAttributes = array_merge($data, $requiredAttributes); - $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/1', $userAgent, $ip, $location, $dataWithAttributes)); - $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/2', $userAgent, $ip, $location, $dataWithAttributes)); - $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'delete', 'database/document/2', $userAgent, $ip, $location, $dataWithAttributes)); - $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log(null, 'insert', 'user/null', $userAgent, $ip, $location, $dataWithAttributes)); + $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/1', $userAgent, $ip, $dataWithAttributes)); + $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/2', $userAgent, $ip, $dataWithAttributes)); + $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'delete', 'database/document/2', $userAgent, $ip, $dataWithAttributes)); + $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log(null, 'insert', 'user/null', $userAgent, $ip, $dataWithAttributes)); } public function testGetLogsByUser(): void @@ -164,12 +163,11 @@ public function testGetLogById(): void $userId = 'testGetByIdUser'; $userAgent = 'Mozilla/5.0 Test'; $ip = '192.168.1.100'; - $location = 'US'; $data = ['test' => 'getById']; $requiredAttributes = $this->getRequiredAttributes(); $dataWithAttributes = array_merge($data, $requiredAttributes); - $log = $this->audit->log($userId, 'create', 'test/resource/123', $userAgent, $ip, $location, $dataWithAttributes); + $log = $this->audit->log($userId, 'create', 'test/resource/123', $userAgent, $ip, $dataWithAttributes); $logId = $log->getId(); // Retrieve the log by ID @@ -182,7 +180,6 @@ public function testGetLogById(): void $this->assertEquals('test/resource/123', $retrievedLog->getAttribute('resource')); $this->assertEquals($userAgent, $retrievedLog->getAttribute('userAgent')); $this->assertEquals($ip, $retrievedLog->getAttribute('ip')); - $this->assertEquals($location, $retrievedLog->getAttribute('location')); $this->assertEquals($data, $retrievedLog->getAttribute('data')); // Test with non-existent ID @@ -198,7 +195,6 @@ public function testLogByBatch(): void $userId = 'batchUserId'; $userAgent = 'Mozilla/5.0 (Test User Agent)'; $ip = '192.168.1.1'; - $location = 'UK'; // Create timestamps 1 minute apart $timestamp1 = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -120)) ?? ''; @@ -212,7 +208,6 @@ public function testLogByBatch(): void 'resource' => 'database/document/batch1', 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['key' => 'value1'], 'time' => $timestamp1 ], @@ -222,7 +217,6 @@ public function testLogByBatch(): void 'resource' => 'database/document/batch2', 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['key' => 'value2'], 'time' => $timestamp2 ], @@ -232,7 +226,6 @@ public function testLogByBatch(): void 'resource' => 'database/document/batch3', 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['key' => 'value3'], 'time' => $timestamp3 ], @@ -242,7 +235,6 @@ public function testLogByBatch(): void 'resource' => 'user1/null', 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['key' => 'value4'], 'time' => $timestamp3 ] @@ -326,7 +318,6 @@ public function testLargeBatchInsert(): void 'resource' => 'doc/' . $i, 'userAgent' => 'Mozilla', 'ip' => '127.0.0.1', - 'location' => 'US', 'data' => ['index' => $i], 'time' => DateTime::formatTz($baseTime) ?? '' ]; @@ -362,7 +353,6 @@ public function testTimeRangeFilters(): void 'resource' => 'doc/1', 'userAgent' => 'Mozilla', 'ip' => '127.0.0.1', - 'location' => 'US', 'data' => [], 'time' => $old ], @@ -372,7 +362,6 @@ public function testTimeRangeFilters(): void 'resource' => 'doc/2', 'userAgent' => 'Mozilla', 'ip' => '127.0.0.1', - 'location' => 'US', 'data' => [], 'time' => $recent ] @@ -404,17 +393,16 @@ public function testCleanup(): void $userId = 'userId'; $userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'; $ip = '127.0.0.1'; - $location = 'US'; $data = ['key1' => 'value1', 'key2' => 'value2']; $requiredAttributes = $this->getRequiredAttributes(); $dataWithAttributes = array_merge($data, $requiredAttributes); - $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/1', $userAgent, $ip, $location, $dataWithAttributes)); + $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/1', $userAgent, $ip, $dataWithAttributes)); sleep(5); - $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/2', $userAgent, $ip, $location, $dataWithAttributes)); + $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/2', $userAgent, $ip, $dataWithAttributes)); sleep(5); - $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'delete', 'database/document/2', $userAgent, $ip, $location, $dataWithAttributes)); + $this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'delete', 'database/document/2', $userAgent, $ip, $dataWithAttributes)); sleep(5); // DELETE logs older than 11 seconds and check that status is true @@ -439,7 +427,6 @@ public function testRetrievalParameters(): void $userId = 'paramtestuser'; $userAgent = 'Mozilla/5.0'; $ip = '192.168.1.1'; - $location = 'US'; // Create 5 logs with different timestamps $baseTime = new \DateTime('2024-06-15 12:00:00'); @@ -455,7 +442,6 @@ public function testRetrievalParameters(): void 'resource' => 'doc/' . $i, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['sequence' => $i], 'time' => $timestamp ]; @@ -611,7 +597,6 @@ public function testFind(): void $userId = 'userId'; $userAgent = 'Mozilla/5.0'; $ip = '192.168.1.1'; - $location = 'US'; // Create test logs with specific attributes $baseTime = new \DateTime('2024-06-15 12:00:00'); @@ -627,7 +612,6 @@ public function testFind(): void 'resource' => 'doc/' . $i, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['sequence' => $i], 'time' => $timestamp ]; @@ -726,7 +710,6 @@ public function testCount(): void $userId = 'userId'; $userAgent = 'Mozilla/5.0'; $ip = '192.168.1.1'; - $location = 'US'; // Create test logs with specific attributes $baseTime = new \DateTime('2024-06-15 12:00:00'); @@ -742,7 +725,6 @@ public function testCount(): void 'resource' => 'doc/' . $i, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['sequence' => $i], 'time' => $timestamp ]; @@ -813,17 +795,17 @@ public function testCount(): void * Apply adapter-specific required attributes to batch events. * * @param array> $batchEvents - * @return array}> + * @return array}> */ protected function applyRequiredAttributesToBatch(array $batchEvents): array { $requiredAttributes = $this->getRequiredAttributes(); if ($requiredAttributes === []) { - /** @var array}> */ + /** @var array}> */ return $batchEvents; } - /** @var array}> */ + /** @var array}> */ return array_map(static fn (array $event) => array_merge($event, $requiredAttributes), $batchEvents); } From 51b1ea72198d4f1d50f21f0a89247e514acbab32 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 17 May 2026 03:29:35 +0000 Subject: [PATCH 2/3] test: cover country round-trip through ClickHouse adapter Country is now resolved at write time by callers (the cloud audit worker resolves the lowercase ISO 3166-1 alpha-2 code from IP via geodb) and supplied through the log data array. Adds a round-trip test covering Audit::log(), Audit::logBatch(), and the no-country fallback. Co-Authored-By: Claude Opus 4.7 --- tests/Audit/Adapter/ClickHouseTest.php | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index 07b138c..9037bc0 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -590,6 +590,64 @@ public function testParseResourceMethod(): void $this->assertEquals('', $parsedOdd['resourceParent']); } + /** + * Country is a write-time ISO 3166-1 alpha-2 code resolved by the caller + * (e.g. cloud's audit worker via geodb). Verify it round-trips through the + * ClickHouse adapter via both Audit::log() and the batch path. + */ + public function testCountryRoundTrip(): void + { + $userId = 'countryUser'; + $userAgent = 'CountryTestAgent/1.0'; + $ip = '8.8.8.8'; + + $data = ['key' => 'value']; + $dataWithAttributes = array_merge($data, $this->getRequiredAttributes(), [ + 'country' => 'us', + ]); + + $log = $this->audit->log($userId, 'create', 'user/' . $userId, $userAgent, $ip, $dataWithAttributes); + + $this->assertEquals('us', $log->getAttribute('country')); + + $retrieved = $this->audit->getLogById($log->getId()); + + $this->assertNotNull($retrieved); + $this->assertEquals('us', $retrieved->getAttribute('country')); + + $batchEvents = [ + [ + 'userId' => $userId, + 'event' => 'batch-create', + 'resource' => 'user/' . $userId . '-batch', + 'userAgent' => $userAgent, + 'ip' => $ip, + 'data' => ['key' => 'batch'], + 'time' => \Utopia\Database\DateTime::formatTz(\Utopia\Database\DateTime::now()) ?? '', + 'country' => 'gb', + ], + ]; + + $batchEvents = $this->applyRequiredAttributesToBatch($batchEvents); + $this->assertTrue($this->audit->logBatch($batchEvents)); + + $batchLogs = $this->audit->getLogsByResource('user/' . $userId . '-batch'); + $this->assertCount(1, $batchLogs); + $this->assertEquals('gb', $batchLogs[0]->getAttribute('country')); + + $missingCountry = $this->audit->log( + $userId, + 'create', + 'user/' . $userId . '-no-country', + $userAgent, + $ip, + array_merge($data, $this->getRequiredAttributes()), + ); + + $missingCountryValue = $missingCountry->getAttribute('country'); + $this->assertTrue($missingCountryValue === null || $missingCountryValue === ''); + } + public function testCursorAfterPaginatesLogs(): void { $page1 = $this->audit->find([ From 153a128996180f077d3ec335698082f504b7c8a9 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 17 May 2026 03:41:00 +0000 Subject: [PATCH 3/3] test: cover country via getRequiredAttributes instead of a dedicated test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Country is a ClickHouse-only schema column (the SQL/Database adapter has no country column — country lives inside the JSON data column there). Adding `country` to ClickHouseTest::getRequiredAttributes() makes every AuditBase test that creates logs against ClickHouse pass the value through the data array, where the adapter then extracts it to the schema column. Same pattern already in use for resourceType, resourceId, projectId, and friends. Drops the dedicated testCountryRoundTrip in favor of this implicit coverage. Co-Authored-By: Claude Opus 4.7 --- tests/Audit/Adapter/ClickHouseTest.php | 59 +------------------------- 1 file changed, 1 insertion(+), 58 deletions(-) diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index 9037bc0..0e3f3f4 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -61,6 +61,7 @@ protected function getRequiredAttributes(): array 'teamId' => 'team-1', 'teamInternalId' => 'team-int-1', 'hostname' => 'example.org', + 'country' => 'us', ]; } @@ -590,64 +591,6 @@ public function testParseResourceMethod(): void $this->assertEquals('', $parsedOdd['resourceParent']); } - /** - * Country is a write-time ISO 3166-1 alpha-2 code resolved by the caller - * (e.g. cloud's audit worker via geodb). Verify it round-trips through the - * ClickHouse adapter via both Audit::log() and the batch path. - */ - public function testCountryRoundTrip(): void - { - $userId = 'countryUser'; - $userAgent = 'CountryTestAgent/1.0'; - $ip = '8.8.8.8'; - - $data = ['key' => 'value']; - $dataWithAttributes = array_merge($data, $this->getRequiredAttributes(), [ - 'country' => 'us', - ]); - - $log = $this->audit->log($userId, 'create', 'user/' . $userId, $userAgent, $ip, $dataWithAttributes); - - $this->assertEquals('us', $log->getAttribute('country')); - - $retrieved = $this->audit->getLogById($log->getId()); - - $this->assertNotNull($retrieved); - $this->assertEquals('us', $retrieved->getAttribute('country')); - - $batchEvents = [ - [ - 'userId' => $userId, - 'event' => 'batch-create', - 'resource' => 'user/' . $userId . '-batch', - 'userAgent' => $userAgent, - 'ip' => $ip, - 'data' => ['key' => 'batch'], - 'time' => \Utopia\Database\DateTime::formatTz(\Utopia\Database\DateTime::now()) ?? '', - 'country' => 'gb', - ], - ]; - - $batchEvents = $this->applyRequiredAttributesToBatch($batchEvents); - $this->assertTrue($this->audit->logBatch($batchEvents)); - - $batchLogs = $this->audit->getLogsByResource('user/' . $userId . '-batch'); - $this->assertCount(1, $batchLogs); - $this->assertEquals('gb', $batchLogs[0]->getAttribute('country')); - - $missingCountry = $this->audit->log( - $userId, - 'create', - 'user/' . $userId . '-no-country', - $userAgent, - $ip, - array_merge($data, $this->getRequiredAttributes()), - ); - - $missingCountryValue = $missingCountry->getAttribute('country'); - $this->assertTrue($missingCountryValue === null || $missingCountryValue === ''); - } - public function testCursorAfterPaginatesLogs(): void { $page1 = $this->audit->find([