CleanWorkToDoTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <?php
  2. namespace App\Tests\Unit\Service\Cron\Job;
  3. use App\Entity\Booking\Course;
  4. use App\Entity\Booking\Work;
  5. use App\Entity\Booking\WorkByUser;
  6. use App\Entity\Core\File;
  7. use App\Service\Cron\Job\CleanWorkToDo;
  8. use App\Service\Cron\UI\CronUIInterface;
  9. use App\Service\File\Storage\LocalStorage;
  10. use App\Service\Utils\DatesUtils;
  11. use Doctrine\Common\Collections\ArrayCollection;
  12. use Doctrine\DBAL\Connection;
  13. use Doctrine\ORM\EntityManagerInterface;
  14. use Doctrine\ORM\Query;
  15. use Doctrine\ORM\QueryBuilder;
  16. use PHPUnit\Framework\MockObject\MockObject;
  17. use PHPUnit\Framework\TestCase;
  18. use Psr\Log\LoggerInterface;
  19. class TestableCleanWorkToDo extends CleanWorkToDo
  20. {
  21. public function deleteWorks(bool $preview = false): void
  22. {
  23. parent::deleteWorks($preview);
  24. }
  25. public function listWorksToDelete(\DateTime $maxDate): array
  26. {
  27. return parent::listWorksToDelete($maxDate);
  28. }
  29. public function previewWorks(array $works): void
  30. {
  31. parent::previewWorks($works);
  32. }
  33. }
  34. class CleanWorkToDoTest extends TestCase
  35. {
  36. private CronUIInterface|MockObject $ui;
  37. private MockObject|LoggerInterface $logger;
  38. private Connection|MockObject $connection;
  39. private EntityManagerInterface|MockObject $entityManager;
  40. private LocalStorage|MockObject $storage;
  41. public function setUp(): void
  42. {
  43. $this->ui = $this->getMockBuilder(CronUIInterface::class)->disableOriginalConstructor()->getMock();
  44. $this->logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
  45. $this->connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
  46. $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
  47. $this->storage = $this->getMockBuilder(LocalStorage::class)->disableOriginalConstructor()->getMock();
  48. }
  49. private function getCleanWorkToDoMockFor(string $methodName): TestableCleanWorkToDo|MockObject
  50. {
  51. $cleanWorkToDo = $this->getMockBuilder(TestableCleanWorkToDo::class)
  52. ->setConstructorArgs([$this->connection, $this->entityManager, $this->storage])
  53. ->setMethodsExcept([$methodName, 'setUI', 'setLoggerInterface'])
  54. ->getMock();
  55. $cleanWorkToDo->setUI($this->ui);
  56. $cleanWorkToDo->setLoggerInterface($this->logger);
  57. return $cleanWorkToDo;
  58. }
  59. /**
  60. * @see CleanWorkToDo::preview()
  61. */
  62. public function testPreview(): void
  63. {
  64. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('preview');
  65. $cleanWorkToDo->expects(self::once())
  66. ->method('deleteWorks')
  67. ->with(true);
  68. $cleanWorkToDo->preview();
  69. }
  70. /**
  71. * @see CleanWorkToDo::execute()
  72. */
  73. public function testExecute(): void
  74. {
  75. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('execute');
  76. $cleanWorkToDo->expects(self::once())
  77. ->method('deleteWorks')
  78. ->with();
  79. $cleanWorkToDo->execute();
  80. }
  81. /**
  82. * @see CleanWorkToDo::deleteWorks()
  83. */
  84. public function testDeleteWorks(): void
  85. {
  86. DatesUtils::setFakeDatetime('2023-12-08 00:00:00');
  87. $expectedMaxDate = DatesUtils::new();
  88. $expectedMaxDate->sub(new \DateInterval('P12M'));
  89. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('deleteWorks');
  90. $work1 = $this->getMockBuilder(Work::class)->getMock();
  91. $work1->method('getId')->willReturn(1);
  92. $work2 = $this->getMockBuilder(Work::class)->getMock();
  93. $work2->method('getId')->willReturn(2);
  94. $works = [$work1, $work2];
  95. $cleanWorkToDo->expects(self::once())
  96. ->method('listWorksToDelete')
  97. ->with($this->callback(function ($date) use ($expectedMaxDate) {
  98. return $date->format('Y-m-d') === $expectedMaxDate->format('Y-m-d');
  99. }))
  100. ->willReturn($works);
  101. $this->ui->expects(self::atLeastOnce())
  102. ->method('print')
  103. ->with('2 works to be removed');
  104. $this->ui->expects(self::exactly(3))
  105. ->method('progress');
  106. $this->connection->expects(self::exactly(2))
  107. ->method('beginTransaction');
  108. $this->connection->expects(self::exactly(2))
  109. ->method('commit');
  110. $this->connection->expects(self::exactly(6))
  111. ->method('executeStatement');
  112. $this->logger->expects(self::exactly(2))
  113. ->method('info')
  114. ->withConsecutive(
  115. [$this->stringContains('Finding works with courses created before')],
  116. [$this->stringContains('Works cleanup completed')]
  117. );
  118. $cleanWorkToDo->deleteWorks();
  119. }
  120. /**
  121. * @see CleanWorkToDo::deleteWorks()
  122. */
  123. public function testDeleteWorksWhenPreview(): void
  124. {
  125. DatesUtils::setFakeDatetime('2023-12-08 00:00:00');
  126. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('deleteWorks');
  127. $work1 = $this->getMockBuilder(Work::class)->getMock();
  128. $work2 = $this->getMockBuilder(Work::class)->getMock();
  129. $works = [$work1, $work2];
  130. $cleanWorkToDo->expects(self::once())
  131. ->method('listWorksToDelete')
  132. ->willReturn($works);
  133. $cleanWorkToDo->expects(self::once())
  134. ->method('previewWorks')
  135. ->with($works);
  136. $this->ui->expects(self::exactly(2))
  137. ->method('print')
  138. ->withConsecutive(
  139. ['2 works to be removed'],
  140. ['> Printing the first and last 10 :']
  141. );
  142. $cleanWorkToDo->deleteWorks(true);
  143. }
  144. /**
  145. * @see CleanWorkToDo::deleteWorks()
  146. */
  147. public function testDeleteWorksWhenNoWorks(): void
  148. {
  149. DatesUtils::setFakeDatetime('2023-12-08 00:00:00');
  150. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('deleteWorks');
  151. $cleanWorkToDo->expects(self::once())
  152. ->method('listWorksToDelete')
  153. ->willReturn([]);
  154. $this->ui->expects(self::once())
  155. ->method('print')
  156. ->with('0 works to be removed');
  157. $this->logger->expects(self::exactly(2))
  158. ->method('info')
  159. ->withConsecutive(
  160. [$this->stringContains('Finding works with courses created before')],
  161. ['No works found to delete']
  162. );
  163. $cleanWorkToDo->deleteWorks();
  164. }
  165. /**
  166. * @see CleanWorkToDo::deleteWorks()
  167. */
  168. public function testDeleteWorksWithNonBlockingError(): void
  169. {
  170. DatesUtils::setFakeDatetime('2023-12-08 00:00:00');
  171. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('deleteWorks');
  172. $work = $this->getMockBuilder(Work::class)->getMock();
  173. $work->method('getId')->willReturn(1);
  174. $works = [$work];
  175. $cleanWorkToDo->expects(self::once())
  176. ->method('listWorksToDelete')
  177. ->willReturn($works);
  178. $this->connection->expects(self::once())
  179. ->method('beginTransaction');
  180. $this->connection->expects(self::once())
  181. ->method('executeStatement')
  182. ->willThrowException(new \RuntimeException('Database error'));
  183. $this->connection->expects(self::once())
  184. ->method('rollback');
  185. $this->logger->expects(self::once())
  186. ->method('error')
  187. ->with('ERROR deleting work 1: Database error');
  188. $cleanWorkToDo->deleteWorks();
  189. }
  190. /**
  191. * @see CleanWorkToDo::deleteWorks()
  192. */
  193. public function testDeleteWorksWithBlockingError(): void
  194. {
  195. DatesUtils::setFakeDatetime('2023-12-08 00:00:00');
  196. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('deleteWorks');
  197. $work = $this->getMockBuilder(Work::class)->getMock();
  198. $work->method('getId')->willReturn(1);
  199. $works = [$work];
  200. $cleanWorkToDo->expects(self::once())
  201. ->method('listWorksToDelete')
  202. ->willReturn($works);
  203. $this->connection->expects(self::once())
  204. ->method('beginTransaction');
  205. $this->connection->expects(self::once())
  206. ->method('executeStatement')
  207. ->willThrowException(new \Exception('Serious database error'));
  208. $this->connection->expects(self::once())
  209. ->method('rollback');
  210. $this->expectException(\Exception::class);
  211. $this->expectExceptionMessage('Serious database error');
  212. $cleanWorkToDo->deleteWorks();
  213. }
  214. /**
  215. * @see CleanWorkToDo::listWorksToDelete()
  216. */
  217. public function testListWorksToDelete(): void
  218. {
  219. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('listWorksToDelete');
  220. $maxDate = new \DateTime('2022-01-01');
  221. $queryBuilder = $this->getMockBuilder(QueryBuilder::class)
  222. ->disableOriginalConstructor()
  223. ->getMock();
  224. $query = $this->getMockBuilder(Query::class)
  225. ->disableOriginalConstructor()
  226. ->getMock();
  227. $work1 = $this->getMockBuilder(Work::class)->getMock();
  228. $work2 = $this->getMockBuilder(Work::class)->getMock();
  229. $expectedResult = [$work1, $work2];
  230. $this->entityManager->expects(self::once())
  231. ->method('createQueryBuilder')
  232. ->willReturn($queryBuilder);
  233. $queryBuilder->expects(self::once())
  234. ->method('select')
  235. ->with('w')
  236. ->willReturnSelf();
  237. $queryBuilder->expects(self::once())
  238. ->method('from')
  239. ->with(Work::class, 'w')
  240. ->willReturnSelf();
  241. $queryBuilder->expects(self::once())
  242. ->method('leftJoin')
  243. ->with('w.course', 'c')
  244. ->willReturnSelf();
  245. $queryBuilder->expects(self::once())
  246. ->method('where')
  247. ->with('c.datetimeStart <= :maxDate OR c.datetimeStart IS NULL')
  248. ->willReturnSelf();
  249. $queryBuilder->expects(self::once())
  250. ->method('setParameter')
  251. ->with('maxDate', $maxDate)
  252. ->willReturnSelf();
  253. $queryBuilder->expects(self::once())
  254. ->method('getQuery')
  255. ->willReturn($query);
  256. $query->expects(self::once())
  257. ->method('getResult')
  258. ->willReturn($expectedResult);
  259. $result = $cleanWorkToDo->listWorksToDelete($maxDate);
  260. $this->assertEquals($expectedResult, $result);
  261. }
  262. /**
  263. * @see CleanWorkToDo::previewWorks()
  264. */
  265. public function testPreviewWorks(): void
  266. {
  267. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('previewWorks');
  268. $course1 = $this->getMockBuilder(Course::class)->getMock();
  269. $course1->method('getName')->willReturn('Test Course 1');
  270. $course2 = $this->getMockBuilder(Course::class)->getMock();
  271. $course2->method('getName')->willReturn('Test Course 2');
  272. $work1 = $this->getMockBuilder(Work::class)->getMock();
  273. $work1->method('getId')->willReturn(1);
  274. $work1->method('getCourse')->willReturn($course1);
  275. $work1->method('getFiles')->willReturn(new ArrayCollection([]));
  276. $work1->method('getWorkByUsers')->willReturn(new ArrayCollection([]));
  277. $work2 = $this->getMockBuilder(Work::class)->getMock();
  278. $work2->method('getId')->willReturn(2);
  279. $work2->method('getCourse')->willReturn($course2);
  280. $work2->method('getFiles')->willReturn(new ArrayCollection([new File(), new File()]));
  281. $work2->method('getWorkByUsers')->willReturn(new ArrayCollection([new WorkByUser()]));
  282. $work3 = $this->getMockBuilder(Work::class)->getMock();
  283. $work3->method('getId')->willReturn(3);
  284. $work3->method('getCourse')->willReturn(null);
  285. $work3->method('getFiles')->willReturn(new ArrayCollection([]));
  286. $work3->method('getWorkByUsers')->willReturn(new ArrayCollection([]));
  287. $works = [$work1, $work2, $work3];
  288. $this->ui->expects(self::exactly(3))
  289. ->method('print')
  290. ->withConsecutive(
  291. [' * Work ID 1 - Course: Test Course 1 - Files: 0 - Users: 0'],
  292. [' * Work ID 2 - Course: Test Course 2 - Files: 2 - Users: 1'],
  293. [' * Work ID 3 - Course: No Course - Files: 0 - Users: 0']
  294. );
  295. $cleanWorkToDo->previewWorks($works);
  296. }
  297. /**
  298. * @see CleanWorkToDo::previewWorks()
  299. */
  300. public function testPreviewWorksWithManyWorks(): void
  301. {
  302. $cleanWorkToDo = $this->getCleanWorkToDoMockFor('previewWorks');
  303. $works = [];
  304. // Create 25 mock works to test the preview logic
  305. for ($i = 1; $i <= 25; ++$i) {
  306. $course = $this->getMockBuilder(Course::class)->getMock();
  307. $course->method('getName')->willReturn("Course $i");
  308. $work = $this->getMockBuilder(Work::class)->getMock();
  309. $work->method('getId')->willReturn($i);
  310. $work->method('getCourse')->willReturn($course);
  311. $work->method('getFiles')->willReturn(new ArrayCollection([]));
  312. $work->method('getWorkByUsers')->willReturn(new ArrayCollection([]));
  313. $works[] = $work;
  314. }
  315. // Expect calls for first 10 works, then "(...)", then last 10 works
  316. $expectedCalls = [];
  317. // First 10
  318. for ($i = 1; $i <= 10; ++$i) {
  319. $expectedCalls[] = [" * Work ID $i - Course: Course $i - Files: 0 - Users: 0"];
  320. }
  321. // Ellipsis
  322. $expectedCalls[] = [' (...)'];
  323. // Last 10
  324. for ($i = 16; $i <= 25; ++$i) {
  325. $expectedCalls[] = [" * Work ID $i - Course: Course $i - Files: 0 - Users: 0"];
  326. }
  327. $this->ui->expects(self::exactly(21))
  328. ->method('print')
  329. ->withConsecutive(...$expectedCalls);
  330. $cleanWorkToDo->previewWorks($works);
  331. }
  332. }