diff --git a/module/CLI/src/Command/Integration/MatomoSendVisitsCommand.php b/module/CLI/src/Command/Integration/MatomoSendVisitsCommand.php
index cacfb9d4..ba9a794e 100644
--- a/module/CLI/src/Command/Integration/MatomoSendVisitsCommand.php
+++ b/module/CLI/src/Command/Integration/MatomoSendVisitsCommand.php
@@ -9,7 +9,6 @@ use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
use Shlinkio\Shlink\Core\Matomo\MatomoVisitSenderInterface;
use Shlinkio\Shlink\Core\Matomo\VisitSendingProgressTrackerInterface;
-use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -18,13 +17,15 @@ use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;
use function Shlinkio\Shlink\Common\buildDateRange;
+use function Shlinkio\Shlink\Core\dateRangeToHumanFriendly;
use function sprintf;
-class MatomoSendVisitsCommand extends Command
+class MatomoSendVisitsCommand extends Command implements VisitSendingProgressTrackerInterface
{
public const NAME = 'integration:matomo:send-visits';
private readonly bool $matomoEnabled;
+ private SymfonyStyle $io;
public function __construct(MatomoOptions $matomoOptions, private readonly MatomoVisitSenderInterface $visitSender)
{
@@ -79,10 +80,10 @@ class MatomoSendVisitsCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $io = new SymfonyStyle($input, $output);
+ $this->io = new SymfonyStyle($input, $output);
if (! $this->matomoEnabled) {
- $io->warning('Matomo integration is not enabled in this Shlink instance');
+ $this->io->warning('Matomo integration is not enabled in this Shlink instance');
return ExitCode::EXIT_WARNING;
}
@@ -95,50 +96,45 @@ class MatomoSendVisitsCommand extends Command
);
if ($input->isInteractive()) {
- // TODO Display the resolved date range in case it didn't fail to parse but the value was incorrect
- $io->warning([
- 'You are about to send visits in this Shlink instance to Matomo',
+ $this->io->warning([
+ 'You are about to send visits from this Shlink instance to Matomo',
+ 'Resolved date range -> ' . dateRangeToHumanFriendly($dateRange),
'Shlink will not check for already sent visits, which could result in some duplications. Make sure '
. 'you have verified only visits in the right date range are going to be sent.',
]);
- if (! $io->confirm('Continue?', default: false)) {
+ if (! $this->io->confirm('Continue?', default: false)) {
return ExitCode::EXIT_WARNING;
}
}
- $result = $this->visitSender->sendVisitsInDateRange(
- $dateRange,
- new class ($io, $this->getApplication()) implements VisitSendingProgressTrackerInterface {
- public function __construct(private readonly SymfonyStyle $io, private readonly ?Application $app)
- {
- }
-
- public function success(int $index): void
- {
- $this->io->write('.');
- }
-
- public function error(int $index, Throwable $e): void
- {
- $this->io->write('E');
- if ($this->io->isVerbose()) {
- $this->app?->renderThrowable($e, $this->io);
- }
- }
- },
- );
+ $result = $this->visitSender->sendVisitsInDateRange($dateRange, $this);
match (true) {
- $result->hasFailures() && $result->hasSuccesses() => $io->warning(
- sprintf('%s visits sent to Matomo. %s failed', $result->successfulVisits, $result->failedVisits),
+ $result->hasFailures() && $result->hasSuccesses() => $this->io->warning(
+ sprintf('%s visits sent to Matomo. %s failed.', $result->successfulVisits, $result->failedVisits),
),
- $result->hasFailures() => $io->error(
- sprintf('%s visits failed to be sent to Matomo.', $result->failedVisits),
+ $result->hasFailures() => $this->io->error(
+ sprintf('Failed to send %s visits to Matomo.', $result->failedVisits),
),
- $result->hasSuccesses() => $io->success(sprintf('%s visits sent to Matomo.', $result->successfulVisits)),
- default => $io->info('There was no visits matching provided date range'),
+ $result->hasSuccesses() => $this->io->success(
+ sprintf('%s visits sent to Matomo.', $result->successfulVisits),
+ ),
+ default => $this->io->info('There was no visits matching provided date range.'),
};
return ExitCode::EXIT_SUCCESS;
}
+
+ public function success(int $index): void
+ {
+ $this->io->write('.');
+ }
+
+ public function error(int $index, Throwable $e): void
+ {
+ $this->io->write('E');
+ if ($this->io->isVerbose()) {
+ $this->getApplication()?->renderThrowable($e, $this->io);
+ }
+ }
}
diff --git a/module/CLI/test/Command/Integration/MatomoSendVisitsCommandTest.php b/module/CLI/test/Command/Integration/MatomoSendVisitsCommandTest.php
new file mode 100644
index 00000000..e3a52733
--- /dev/null
+++ b/module/CLI/test/Command/Integration/MatomoSendVisitsCommandTest.php
@@ -0,0 +1,135 @@
+visitSender = $this->createMock(MatomoVisitSenderInterface::class);
+ }
+
+ #[Test]
+ public function warningDisplayedIfIntegrationIsNotEnabled(): void
+ {
+ [$output, $exitCode] = $this->executeCommand(matomoEnabled: false);
+
+ self::assertStringContainsString('Matomo integration is not enabled in this Shlink instance', $output);
+ self::assertEquals(ExitCode::EXIT_WARNING, $exitCode);
+ }
+
+ #[Test]
+ #[TestWith([true])]
+ #[TestWith([false])]
+ public function warningIsOnlyDisplayedInInteractiveMode(bool $interactive): void
+ {
+ $this->visitSender->method('sendVisitsInDateRange')->willReturn(new SendVisitsResult());
+
+ [$output] = $this->executeCommand(['y'], ['interactive' => $interactive]);
+
+ if ($interactive) {
+ self::assertStringContainsString('You are about to send visits', $output);
+ } else {
+ self::assertStringNotContainsString('You are about to send visits', $output);
+ }
+ }
+
+ #[Test]
+ #[TestWith([true])]
+ #[TestWith([false])]
+ public function canCancelExecutionInInteractiveMode(bool $interactive): void
+ {
+ $this->visitSender->expects($this->exactly($interactive ? 0 : 1))->method('sendVisitsInDateRange')->willReturn(
+ new SendVisitsResult(),
+ );
+ $this->executeCommand(['n'], ['interactive' => $interactive]);
+ }
+
+ #[Test]
+ #[TestWith([new SendVisitsResult(), 'There was no visits matching provided date range'])]
+ #[TestWith([new SendVisitsResult(successfulVisits: 10), '10 visits sent to Matomo.'])]
+ #[TestWith([new SendVisitsResult(successfulVisits: 2), '2 visits sent to Matomo.'])]
+ #[TestWith([new SendVisitsResult(failedVisits: 238), 'Failed to send 238 visits to Matomo.'])]
+ #[TestWith([new SendVisitsResult(failedVisits: 18), 'Failed to send 18 visits to Matomo.'])]
+ #[TestWith([new SendVisitsResult(successfulVisits: 2, failedVisits: 35), '2 visits sent to Matomo. 35 failed.'])]
+ #[TestWith([new SendVisitsResult(successfulVisits: 81, failedVisits: 6), '81 visits sent to Matomo. 6 failed.'])]
+ public function expectedResultIsDisplayed(SendVisitsResult $result, string $expectedResultMessage): void
+ {
+ $this->visitSender->expects($this->once())->method('sendVisitsInDateRange')->willReturn($result);
+ [$output, $exitCode] = $this->executeCommand(['y']);
+
+ self::assertStringContainsString($expectedResultMessage, $output);
+ self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
+ }
+
+ #[Test]
+ public function printsResultOfSendingVisits(): void
+ {
+ $this->visitSender->method('sendVisitsInDateRange')->willReturnCallback(
+ function (DateRange $_, MatomoSendVisitsCommand $command): SendVisitsResult {
+ // Call it a few times for an easier match of its result in the command putput
+ $command->success(0);
+ $command->success(1);
+ $command->success(2);
+ $command->error(3, new Exception('Error'));
+ $command->success(4);
+ $command->error(5, new Exception('Error'));
+
+ return new SendVisitsResult();
+ },
+ );
+
+ [$output] = $this->executeCommand(['y']);
+
+ self::assertStringContainsString('...E.E', $output);
+ }
+
+ #[Test]
+ #[TestWith([[], 'All time'])]
+ #[TestWith([['--since' => '2023-05-01'], 'Since 2023-05-01 00:00:00'])]
+ #[TestWith([['--until' => '2023-05-01'], 'Until 2023-05-01 00:00:00'])]
+ #[TestWith([
+ ['--since' => '2023-05-01', '--until' => '2024-02-02 23:59:59'],
+ 'Between 2023-05-01 00:00:00 and 2024-02-02 23:59:59',
+ ])]
+ public function providedDateAreParsed(array $args, string $expectedMessage): void
+ {
+ [$output] = $this->executeCommand(['n'], args: $args);
+ self::assertStringContainsString('Resolved date range -> ' . $expectedMessage, $output);
+ }
+
+ /**
+ * @return array{string, int, MatomoSendVisitsCommand}
+ */
+ private function executeCommand(
+ array $input = [],
+ array $options = [],
+ array $args = [],
+ bool $matomoEnabled = true,
+ ): array {
+ $command = new MatomoSendVisitsCommand(new MatomoOptions(enabled: $matomoEnabled), $this->visitSender);
+ $commandTester = CliTestUtils::testerForCommand($command);
+ $commandTester->setInputs($input);
+ $commandTester->execute($args, $options);
+
+ $output = $commandTester->getDisplay();
+ $exitCode = $commandTester->getStatusCode();
+
+ return [$output, $exitCode, $command];
+ }
+}
diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php
index 5ba45ac2..2e238e99 100644
--- a/module/Core/functions/functions.php
+++ b/module/Core/functions/functions.php
@@ -61,6 +61,23 @@ function parseDateRangeFromQuery(array $query, string $startDateName, string $en
return buildDateRange($startDate, $endDate);
}
+function dateRangeToHumanFriendly(?DateRange $dateRange): string
+{
+ $startDate = $dateRange?->startDate;
+ $endDate = $dateRange?->endDate;
+
+ return match (true) {
+ $startDate !== null && $endDate !== null => sprintf(
+ 'Between %s and %s',
+ $startDate->toDateTimeString(),
+ $endDate->toDateTimeString(),
+ ),
+ $startDate !== null => sprintf('Since %s', $startDate->toDateTimeString()),
+ $endDate !== null => sprintf('Until %s', $endDate->toDateTimeString()),
+ default => 'All time',
+ };
+}
+
/**
* @return ($date is null ? null : Chronos)
*/
diff --git a/module/Core/test/Matomo/MatomoVisitSenderTest.php b/module/Core/test/Matomo/MatomoVisitSenderTest.php
index aebdf7e0..816c8eea 100644
--- a/module/Core/test/Matomo/MatomoVisitSenderTest.php
+++ b/module/Core/test/Matomo/MatomoVisitSenderTest.php
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Matomo;
use Exception;
-use Laminas\Validator\Date;
use MatomoTracker;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;