diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d4e3ac8..4dc888493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this Previously, this was exposed only for orphan visits, since this can be an arbitrary value for those. * [#2077](https://github.com/shlinkio/shlink/issues/2077) When sending visits to Matomo, the short URL title is now used as document title in matomo. +* [#2059](https://github.com/shlinkio/shlink/issues/2059) Add new `short-url:delete-expired` command that can be used to programmatically delete expired short URLs. + + Expired short URLs are those that have a `calidUntil` date in the past, or optionally, that have reached the max amount of visits. + + This command can be run periodically by those who create many disposable URLs which are valid only for a period of time, and then can be deleted to save space. ### Changed * [#2034](https://github.com/shlinkio/shlink/issues/2034) Modernize entities, using constructor property promotion and readonly wherever possible. diff --git a/module/CLI/test/Command/ShortUrl/DeleteExpiredShortUrlsCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteExpiredShortUrlsCommandTest.php new file mode 100644 index 000000000..5930a93c4 --- /dev/null +++ b/module/CLI/test/Command/ShortUrl/DeleteExpiredShortUrlsCommandTest.php @@ -0,0 +1,90 @@ +service = $this->createMock(DeleteShortUrlServiceInterface::class); + $this->commandTester = CliTestUtils::testerForCommand(new DeleteExpiredShortUrlsCommand($this->service)); + } + + #[Test] + public function warningIsDisplayedAndExecutionCanBeCancelled(): void + { + $this->service->expects($this->never())->method('countExpiredShortUrls'); + $this->service->expects($this->never())->method('deleteExpiredShortUrls'); + + $this->commandTester->setInputs(['n']); + $this->commandTester->execute([]); + $output = $this->commandTester->getDisplay(); + $status = $this->commandTester->getStatusCode(); + + self::assertStringContainsString('Careful!', $output); + self::assertEquals(ExitCode::EXIT_WARNING, $status); + } + + #[Test] + #[TestWith([[], true])] + #[TestWith([['--force' => true], false])] + #[TestWith([['-f' => true], false])] + public function deletionIsExecutedByDefault(array $input, bool $expectsWarning): void + { + $this->service->expects($this->never())->method('countExpiredShortUrls'); + $this->service->expects($this->once())->method('deleteExpiredShortUrls')->willReturn(5); + + $this->commandTester->setInputs(['y']); + $this->commandTester->execute($input); + $output = $this->commandTester->getDisplay(); + $status = $this->commandTester->getStatusCode(); + + if ($expectsWarning) { + self::assertStringContainsString('Careful!', $output); + } else { + self::assertStringNotContainsString('Careful!', $output); + } + self::assertStringContainsString('5 expired short URLs have been deleted', $output); + self::assertEquals(ExitCode::EXIT_SUCCESS, $status); + } + + #[Test] + public function countIsExecutedDuringDryRun(): void + { + $this->service->expects($this->once())->method('countExpiredShortUrls')->willReturn(38); + $this->service->expects($this->never())->method('deleteExpiredShortUrls'); + + $this->commandTester->execute(['--dry-run' => true]); + $output = $this->commandTester->getDisplay(); + $status = $this->commandTester->getStatusCode(); + + self::assertStringNotContainsString('Careful!', $output); + self::assertStringContainsString('There are 38 expired short URLs matching provided conditions', $output); + self::assertEquals(ExitCode::EXIT_SUCCESS, $status); + } + + #[Test] + #[TestWith([[], new ExpiredShortUrlsConditions()])] + #[TestWith([['--evaluate-max-visits' => true], new ExpiredShortUrlsConditions(maxVisitsReached: true)])] + public function providesExpectedConditionsToService(array $extraInput, ExpiredShortUrlsConditions $conditions): void + { + $this->service->expects($this->once())->method('countExpiredShortUrls')->with($conditions)->willReturn(4); + $this->commandTester->execute(['--dry-run' => true, ...$extraInput]); + } +}