CleanWorkToDoTest.php 14 KB

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