From 49eea8c480c35b5a83b045c8e72851c62a39c368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Jodas?= Date: Fri, 3 Oct 2025 12:06:00 +0200 Subject: [PATCH 1/4] skip orphaned manifests --- src/Extractor/Extractor.php | 27 ++++++++++++++++--- src/Extractor/Paginator/IPaginator.php | 2 +- src/Extractor/Paginator/ProfilesPaginator.php | 4 ++- .../Paginator/PropertiesPaginator.php | 4 ++- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/Extractor/Extractor.php b/src/Extractor/Extractor.php index 199d9be..8c27842 100644 --- a/src/Extractor/Extractor.php +++ b/src/Extractor/Extractor.php @@ -60,10 +60,10 @@ public function runProfiles(array $query, array $profiles): array if (isset($query['query'])) { $outputCsv = $this->output->createReport($query); - $this->output->createManifest($outputCsv->getFilename(), $query, ['id'], true); $this->logger->info(sprintf("Running query '%s'", $query['outputTable'])); $downloadedProfiles = false; + $manifestCreated = false; foreach ($profiles as $profile) { $this->logger->info(sprintf('Profile "%s" export started.', $profile['id'])); $apiQuery = $query; @@ -130,7 +130,16 @@ public function runProfiles(array $query, array $profiles): array } } - $paginator->paginate($apiQuery, $report, $outputCsv); + $rowCount = $paginator->paginate($apiQuery, $report, $outputCsv); + if ($rowCount > 0 && !$manifestCreated) { + $this->output->createManifest( + $outputCsv->getFilename(), + $query, + ['id'], + true, + ); + $manifestCreated = true; + } $status[$query['outputTable']][$profile['id']] = 'ok'; } @@ -159,10 +168,10 @@ public function runProperties(array $query, array $properties): array $query['query']['endpoint'] = 'properties'; $outputCsv = $this->output->createReport($query); - $this->output->createManifest($outputCsv->getFilename(), $query, ['id'], true, 'idProperty'); $this->logger->info(sprintf("Running query '%s'", $query['outputTable'])); $downloadedProperties = false; + $manifestCreated = false; foreach ($properties as $property) { $this->logger->info(sprintf('Property "%s" export started.', $property['propertyName'])); if (!empty($query['query']['viewId']) @@ -199,7 +208,17 @@ public function runProperties(array $query, array $properties): array continue; } - $paginator->paginate($apiQuery, $report, $outputCsv); + $rowCount = $paginator->paginate($apiQuery, $report, $outputCsv); + if ($rowCount > 0 && !$manifestCreated) { + $this->output->createManifest( + $outputCsv->getFilename(), + $query, + ['id'], + true, + 'idProperty', + ); + $manifestCreated = true; + } $status[$query['outputTable']][$property['propertyKey']] = 'ok'; } diff --git a/src/Extractor/Paginator/IPaginator.php b/src/Extractor/Paginator/IPaginator.php index 2e8bebf..efa79dc 100644 --- a/src/Extractor/Paginator/IPaginator.php +++ b/src/Extractor/Paginator/IPaginator.php @@ -12,5 +12,5 @@ interface IPaginator { public function getOutput(): Output; public function getClient(): Client; - public function paginate(array $query, array $report, CsvFile $csvFile): void; + public function paginate(array $query, array $report, CsvFile $csvFile): int; } diff --git a/src/Extractor/Paginator/ProfilesPaginator.php b/src/Extractor/Paginator/ProfilesPaginator.php index 3bf2cc4..b19b88b 100644 --- a/src/Extractor/Paginator/ProfilesPaginator.php +++ b/src/Extractor/Paginator/ProfilesPaginator.php @@ -34,7 +34,7 @@ public function getClient(): Client return $this->client; } - public function paginate(array $query, array $report, CsvFile $csvFile): void + public function paginate(array $query, array $report, CsvFile $csvFile): int { $counter = 0; do { @@ -60,6 +60,8 @@ public function paginate(array $query, array $report, CsvFile $csvFile): void } $query = $nextQuery; } while ($query); + + return $counter; } private function getStartIndex(string $link): string diff --git a/src/Extractor/Paginator/PropertiesPaginator.php b/src/Extractor/Paginator/PropertiesPaginator.php index c3750c4..13bdff2 100644 --- a/src/Extractor/Paginator/PropertiesPaginator.php +++ b/src/Extractor/Paginator/PropertiesPaginator.php @@ -44,7 +44,7 @@ public function setProperty(array $property): self return $this; } - public function paginate(array $query, array $report, CsvFile $csvFile): void + public function paginate(array $query, array $report, CsvFile $csvFile): int { $localCounter = 0; do { @@ -66,5 +66,7 @@ public function paginate(array $query, array $report, CsvFile $csvFile): void $query = $nextQuery; } while ($report['totals'] > $localCounter); + + return $localCounter; } } From 348af14fc312bc9b2c193fe62d0ef02c1de6da62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Jodas?= Date: Fri, 3 Oct 2025 12:20:39 +0200 Subject: [PATCH 2/4] remove GA4 tests --- phpstan-baseline.neon | 10 --- .../ApplicationTest.php | 73 ------------------- tests/data/config_antisampling.json | 52 ------------- tests/data/config_antisampling_adaptive.json | 53 -------------- tests/data/config_mcf.json | 63 ---------------- 5 files changed, 251 deletions(-) delete mode 100644 tests/data/config_antisampling.json delete mode 100644 tests/data/config_antisampling_adaptive.json delete mode 100644 tests/data/config_mcf.json diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0d514c7..50883ca 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -840,11 +840,6 @@ parameters: count: 1 path: tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php - - - message: "#^Method Keboola\\\\GoogleAnalyticsExtractor\\\\ApplicationTest\\:\\:assertManifestContainsColumns\\(\\) has parameter \\$expected with no value type specified in iterable type array\\.$#" - count: 1 - path: tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php - - message: "#^Method Keboola\\\\GoogleAnalyticsExtractor\\\\ApplicationTest\\:\\:getConfig\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -855,11 +850,6 @@ parameters: count: 1 path: tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 1 - path: tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php - - message: "#^Parameter \\#2 \\$array of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) expects array\\|ArrayAccess, mixed given\\.$#" count: 2 diff --git a/tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php b/tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php index f4032ff..a473bce 100644 --- a/tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php +++ b/tests/Keboola/GoogleAnalyticsExtractor/ApplicationTest.php @@ -50,72 +50,6 @@ private function getConfig(string $suffix = ''): array return $config; } - public function testAppRunDailyWalk(): void - { - $this->config = $this->getConfig('_antisampling'); - $this->runProcess(); - - $dailyWalk = $this->getManifestFiles('dailyWalk'); - Assert::assertEquals(1, count($dailyWalk)); - - foreach ($dailyWalk as $file) { - /** @var $file SplFileInfo */ - $this->assertManifestContainsColumns($file->getPathname(), [ - 'id', - 'idProfile', - 'date', - 'sourceMedium', - 'landingPagePath', - 'pageviews', - ]); - } - } - - public function testAppRunAdaptive(): void - { - $this->config = $this->getConfig('_antisampling_adaptive'); - $this->runProcess(); - - $adaptive = $this->getManifestFiles('adaptive'); - Assert::assertEquals(1, count($adaptive)); - - foreach ($adaptive as $file) { - /** @var $file SplFileInfo */ - $this->assertManifestContainsColumns($file->getPathname(), [ - 'id', - 'idProfile', - 'date', - 'sourceMedium', - 'landingPagePath', - 'pageviews', - ]); - } - } - - public function testAppRunMCF(): void - { - $this->config = $this->getConfig('_mcf'); - $this->runProcess(); - - $funnelFiles = $this->getManifestFiles('funnel'); - Assert::assertEquals(1, count($funnelFiles)); - - foreach ($funnelFiles as $file) { - /** @var $file SplFileInfo */ - $this->assertManifestContainsColumns($file->getPathname(), [ - 'id', - 'idProfile', - 'mcf:conversionDate', - 'mcf:sourcePath', - 'mcf:mediumPath', - 'mcf:sourceMedium', - 'mcf:totalConversions', - 'mcf:totalConversionValue', - 'mcf:assistedConversions', - ]); - } - } - public function testAppProfilesProperties(): void { $this->config = $this->getConfig('_empty'); @@ -183,13 +117,6 @@ private function getManifestFiles(string $queryName): Finder ; } - private function assertManifestContainsColumns(string $pathname, array $expected): void - { - $manifest = (array) json_decode(file_get_contents($pathname), true, 512, JSON_THROW_ON_ERROR); - Assert::assertArrayHasKey('columns', $manifest); - Assert::assertEquals($expected, $manifest['columns']); - } - public function appRunDataProvider(): Generator { yield 'configRow' => [ diff --git a/tests/data/config_antisampling.json b/tests/data/config_antisampling.json deleted file mode 100644 index 7bfd79a..0000000 --- a/tests/data/config_antisampling.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "parameters": { - "retriesCount": 1, - "outputBucket": "in.c-ex-google-analytics-cfg1", - "profiles": [ - { - "id": 184062725, - "name": "All Web Site Data", - "webPropertyId": "UA-128209249-1", - "webPropertyName": "Keboola Website", - "accountId": 128209249, - "accountName": "Keboola Website" - }, - { - "id": 88156763, - "name": "All Web Site Data", - "webPropertyId": "UA-128209249-1", - "webPropertyName": "status.keboola.com", - "accountId": 128209249, - "accountName": "Keboola Status" - } - ], - "outputTable": "dailyWalk", - "antisampling": "dailyWalk", - "query": { - "metrics": [ - { - "expression": "ga:pageviews" - } - ], - "dimensions": [ - { - "name": "ga:date" - }, - { - "name": "ga:sourceMedium" - }, - { - "name": "ga:landingPagePath" - } - ], - "filtersExpression": "", - "segments": null, - "dateRanges": [ - { - "startDate": "-3 days", - "endDate": "-1 day" - } - ] - } - } -} diff --git a/tests/data/config_antisampling_adaptive.json b/tests/data/config_antisampling_adaptive.json deleted file mode 100644 index b8f568e..0000000 --- a/tests/data/config_antisampling_adaptive.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "parameters": { - "retriesCount": 1, - "outputBucket": "in.c-ex-google-analytics-cfg1", - "profiles": [ - { - "id": 184062725, - "name": "All Web Site Data", - "webPropertyId": "UA-128209249-1", - "webPropertyName": "Keboola Website", - "accountId": 128209249, - "accountName": "Keboola Website" - }, - { - "id": 88156763, - "name": "All Web Site Data", - "webPropertyId": "UA-128209249-1", - "webPropertyName": "status.keboola.com", - "accountId": 128209249, - "accountName": "Keboola Status" - } - ], - "outputTable": "adaptive", - "antisampling": "adaptive", - "query": { - "viewId": "26550866", - "metrics": [ - { - "expression": "ga:pageviews" - } - ], - "dimensions": [ - { - "name": "ga:date" - }, - { - "name": "ga:sourceMedium" - }, - { - "name": "ga:landingPagePath" - } - ], - "filtersExpression": "", - "segments": null, - "dateRanges": [ - { - "startDate": "-3 days", - "endDate": "-1 day" - } - ] - } - } -} diff --git a/tests/data/config_mcf.json b/tests/data/config_mcf.json deleted file mode 100644 index c6c4d3e..0000000 --- a/tests/data/config_mcf.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "parameters": { - "outputBucket": "in.c-ex-google-analytics-cfg1", - "retriesCount": 1, - "profiles": [ - { - "id": 184062725, - "name": "All Web Site Data", - "webPropertyId": "UA-128209249-1", - "webPropertyName": "Keboola Website", - "accountId": 128209249, - "accountName": "Keboola Website" - }, - { - "id": 88156763, - "name": "All Web Site Data", - "webPropertyId": "UA-128209249-1", - "webPropertyName": "status.keboola.com", - "accountId": 128209249, - "accountName": "Keboola Status" - } - ], - "outputTable": "funnel", - "endpoint": "mcf", - "antisampling": "dailyWalk", - "query": { - "samplingLevel": "FASTER", - "maxResults": 100, - "metrics": [ - { - "expression": "mcf:totalConversions" - }, - { - "expression": "mcf:totalConversionValue" - }, - { - "expression": "mcf:assistedConversions" - } - ], - "dimensions": [ - { - "name": "mcf:conversionDate" - }, - { - "name": "mcf:sourcePath" - }, - { - "name": "mcf:mediumPath" - }, - { - "name": "mcf:sourceMedium" - } - ], - "filtersExpression": "", - "dateRanges": [ - { - "startDate": "-1 week", - "endDate": "-1 day" - } - ] - } - } -} From 0493ef81d446bb015fdd23fa57af777efc88996c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Jodas?= Date: Fri, 3 Oct 2025 12:20:44 +0200 Subject: [PATCH 3/4] add test for empty data --- .../expected/data/out/files/.gitkeep | 0 .../expected/data/out/tables/properties.csv | 1 + .../data/out/tables/properties.csv.manifest | 1 + .../expected/data/out/usage.json | 1 + .../source/data/config.json | 54 +++++++++++++++++++ 5 files changed, 57 insertions(+) create mode 100644 tests/functional/download-empty-data/expected/data/out/files/.gitkeep create mode 100644 tests/functional/download-empty-data/expected/data/out/tables/properties.csv create mode 100644 tests/functional/download-empty-data/expected/data/out/tables/properties.csv.manifest create mode 100644 tests/functional/download-empty-data/expected/data/out/usage.json create mode 100644 tests/functional/download-empty-data/source/data/config.json diff --git a/tests/functional/download-empty-data/expected/data/out/files/.gitkeep b/tests/functional/download-empty-data/expected/data/out/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/functional/download-empty-data/expected/data/out/tables/properties.csv b/tests/functional/download-empty-data/expected/data/out/tables/properties.csv new file mode 100644 index 0000000..1b7864b --- /dev/null +++ b/tests/functional/download-empty-data/expected/data/out/tables/properties.csv @@ -0,0 +1 @@ +"properties/403517979","Website - GA4","accounts/128209249","Keboola Website" diff --git a/tests/functional/download-empty-data/expected/data/out/tables/properties.csv.manifest b/tests/functional/download-empty-data/expected/data/out/tables/properties.csv.manifest new file mode 100644 index 0000000..331dcfe --- /dev/null +++ b/tests/functional/download-empty-data/expected/data/out/tables/properties.csv.manifest @@ -0,0 +1 @@ +{"destination":"in.c-keboola-ex-google-analytics-v4-01k6mq8atddf0xbr48p2b9st4h.properties","incremental":true,"columns":["propertyKey","propertyName","accountKey","accountName"],"primary_key":["propertyKey"],"column_metadata":{"propertyKey":[{"key":"KBC.datatype.nullable","value":true},{"key":"KBC.datatype.basetype","value":"STRING"}],"propertyName":[{"key":"KBC.datatype.nullable","value":true},{"key":"KBC.datatype.basetype","value":"STRING"}],"accountKey":[{"key":"KBC.datatype.nullable","value":true},{"key":"KBC.datatype.basetype","value":"STRING"}],"accountName":[{"key":"KBC.datatype.nullable","value":true},{"key":"KBC.datatype.basetype","value":"STRING"}]}} \ No newline at end of file diff --git a/tests/functional/download-empty-data/expected/data/out/usage.json b/tests/functional/download-empty-data/expected/data/out/usage.json new file mode 100644 index 0000000..7121ffb --- /dev/null +++ b/tests/functional/download-empty-data/expected/data/out/usage.json @@ -0,0 +1 @@ +[{"metric":"API Calls","value":1}] \ No newline at end of file diff --git a/tests/functional/download-empty-data/source/data/config.json b/tests/functional/download-empty-data/source/data/config.json new file mode 100644 index 0000000..dd7b4e2 --- /dev/null +++ b/tests/functional/download-empty-data/source/data/config.json @@ -0,0 +1,54 @@ +{ + "authorization": { + "oauth_api": { + "credentials": { + "appKey": "%env(string:CLIENT_ID)%", + "#appSecret": "%env(string:CLIENT_SECRET)%", + "#data": "%env(string:CREDENTIALS_DATA)%" + } + } + }, + "parameters": { + "outputTable": "empty-data", + "query": { + "metrics": [ + { + "name": "active1DayUsers" + }, + { + "name": "active28DayUsers" + } + ], + "dimensions": [ + { + "name": "campaignId" + }, + { + "name": "campaignName" + }, + { + "name": "city" + }, + { + "name": "cityId" + } + ], + "dateRanges": [ + { + "startDate": "2015-08-14", + "endDate": "2015-08-15" + } + ] + }, + "endpoint": "data-api", + "outputBucket": "in.c-keboola-ex-google-analytics-v4-01k6mq8atddf0xbr48p2b9st4h", + "properties": [ + { + "accountKey": "accounts/128209249", + "accountName": "Keboola Website", + "propertyKey": "properties/403517979", + "propertyName": "Website - GA4" + } + ] + } +} From 11daa4107a559ea03b246e7e8caf8fe12eed5bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Jodas?= Date: Fri, 3 Oct 2025 14:59:48 +0200 Subject: [PATCH 4/4] add tests for paginator --- .../Paginator/PropertiesPaginatorTest.php | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 tests/Keboola/GoogleAnalyticsExtractor/Extractor/Paginator/PropertiesPaginatorTest.php diff --git a/tests/Keboola/GoogleAnalyticsExtractor/Extractor/Paginator/PropertiesPaginatorTest.php b/tests/Keboola/GoogleAnalyticsExtractor/Extractor/Paginator/PropertiesPaginatorTest.php new file mode 100644 index 0000000..b3ffe2c --- /dev/null +++ b/tests/Keboola/GoogleAnalyticsExtractor/Extractor/Paginator/PropertiesPaginatorTest.php @@ -0,0 +1,285 @@ +output = $this->createMock(Output::class); + $this->client = $this->createMock(Client::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->paginator = new PropertiesPaginator( + $this->output, + $this->client, + $this->logger, + ); + } + + public function testPaginateSinglePage(): void + { + $property = [ + 'propertyKey' => 'properties/123456789', + 'propertyName' => 'Test Property', + ]; + + $query = [ + 'query' => [ + 'dimensions' => [['name' => 'ga:date']], + 'metrics' => [['name' => 'ga:sessions']], + 'dateRanges' => [ + ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], + ], + ], + ]; + + $report = [ + 'data' => [ + new Result(['ga:sessions' => '100'], ['ga:date' => '2023-01-01']), + new Result(['ga:sessions' => '150'], ['ga:date' => '2023-01-02']), + ], + 'totals' => 2, + 'rowCount' => 2, + ]; + + /** @var CsvFile $csvFile */ + $csvFile = $this->createMock(CsvFile::class); + + $this->paginator->setProperty($property); + + // Expect writeReport to be called once with the correct property ID + $this->output->expects($this->once()) + ->method('writeReport') + ->with($csvFile, $report, '123456789'); + + // Expect logger to be called with progress info + $this->logger->expects($this->once()) + ->method('info') + ->with('Downloaded 2/2 records.'); + + $result = $this->paginator->paginate($query, $report, $csvFile); + + $this->assertEquals(2, $result); + } + + public function testPaginateMultiplePages(): void + { + $property = [ + 'propertyKey' => 'properties/123456789', + 'propertyName' => 'Test Property', + ]; + + $query = [ + 'query' => [ + 'dimensions' => [['name' => 'ga:date']], + 'metrics' => [['name' => 'ga:sessions']], + 'dateRanges' => [ + ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], + ], + ], + ]; + + $firstReport = [ + 'data' => [ + new Result(['ga:sessions' => '100'], ['ga:date' => '2023-01-01']), + new Result(['ga:sessions' => '150'], ['ga:date' => '2023-01-02']), + ], + 'totals' => 4, + 'rowCount' => 2, + ]; + + $secondReport = [ + 'data' => [ + new Result(['ga:sessions' => '200'], ['ga:date' => '2023-01-03']), + new Result(['ga:sessions' => '250'], ['ga:date' => '2023-01-04']), + ], + 'totals' => 4, + 'rowCount' => 2, + ]; + + /** @var CsvFile $csvFile */ + $csvFile = $this->createMock(CsvFile::class); + + $this->paginator->setProperty($property); + + // Expect writeReport to be called twice + $this->output->expects($this->exactly(2)) + ->method('writeReport') + ->with($csvFile, $this->isType('array'), '123456789'); + + // Expect logger to be called twice with progress info + $this->logger->expects($this->exactly(2)) + ->method('info') + ->willReturnOnConsecutiveCalls( + $this->returnValue(null), + $this->returnValue(null), + ); + + // Expect client to be called once for the second page + $this->client->expects($this->once()) + ->method('getPropertyReport') + ->with( + [ + 'query' => [ + 'dimensions' => [['name' => 'ga:date']], + 'metrics' => [['name' => 'ga:sessions']], + 'dateRanges' => [ + ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], + ], + 'offset' => 2, + ], + ], + $property, + ) + ->willReturn($secondReport); + + $result = $this->paginator->paginate($query, $firstReport, $csvFile); + + $this->assertEquals(4, $result); + } + + public function testPaginateEmptyData(): void + { + $property = [ + 'propertyKey' => 'properties/123456789', + 'propertyName' => 'Test Property', + ]; + + $query = [ + 'query' => [ + 'dimensions' => [['name' => 'ga:date']], + 'metrics' => [['name' => 'ga:sessions']], + 'dateRanges' => [ + ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], + ], + ], + ]; + + $report = [ + 'data' => [], + 'totals' => 0, + 'rowCount' => 0, + ]; + + /** @var CsvFile $csvFile */ + $csvFile = $this->createMock(CsvFile::class); + + $this->paginator->setProperty($property); + + // Expect writeReport to be called once even with empty data + $this->output->expects($this->once()) + ->method('writeReport') + ->with($csvFile, $report, '123456789'); + + // Expect logger to be called with progress info + $this->logger->expects($this->once()) + ->method('info') + ->with('Downloaded 0/0 records.'); + + $result = $this->paginator->paginate($query, $report, $csvFile); + + $this->assertEquals(0, $result); + } + + public function testPaginateWithOffsetInQuery(): void + { + $property = [ + 'propertyKey' => 'properties/987654321', + 'propertyName' => 'Test Property 2', + ]; + + $query = [ + 'query' => [ + 'dimensions' => [['name' => 'ga:date']], + 'metrics' => [['name' => 'ga:sessions']], + 'dateRanges' => [ + ['startDate' => '2023-01-01', 'endDate' => '2023-01-31'], + ], + 'offset' => 10, // Existing offset should be preserved + ], + ]; + + $firstReport = [ + 'data' => [ + new Result(['ga:sessions' => '100'], ['ga:date' => '2023-01-01']), + ], + 'totals' => 3, + 'rowCount' => 1, + ]; + + $secondReport = [ + 'data' => [ + new Result(['ga:sessions' => '200'], ['ga:date' => '2023-01-02']), + ], + 'totals' => 3, + 'rowCount' => 1, + ]; + + $thirdReport = [ + 'data' => [ + new Result(['ga:sessions' => '300'], ['ga:date' => '2023-01-03']), + ], + 'totals' => 3, + 'rowCount' => 1, + ]; + + /** @var CsvFile $csvFile */ + $csvFile = $this->createMock(CsvFile::class); + + $this->paginator->setProperty($property); + + // Expect writeReport to be called three times + $this->output->expects($this->exactly(3)) + ->method('writeReport') + ->with($csvFile, $this->isType('array'), '987654321'); + + // Expect logger to be called three times + $this->logger->expects($this->exactly(3)) + ->method('info') + ->willReturnOnConsecutiveCalls( + $this->returnValue(null), + $this->returnValue(null), + $this->returnValue(null), + ); + + // Expect client to be called twice for subsequent pages + $callCount = 0; + $this->client->expects($this->exactly(2)) + ->method('getPropertyReport') + ->willReturnCallback(function ($query, $property) use (&$callCount, $secondReport, $thirdReport) { + $callCount++; + if ($callCount === 1) { + $this->assertEquals(1, $query['query']['offset']); + return $secondReport; + } else { + $this->assertEquals(2, $query['query']['offset']); + return $thirdReport; + } + }); + + $result = $this->paginator->paginate($query, $firstReport, $csvFile); + + $this->assertEquals(3, $result); + } + + public function testGetOutput(): void + { + $this->assertSame($this->output, $this->paginator->getOutput()); + } +}