client->request('GET', '/inventory/'); self::assertResponseIsSuccessful(); self::assertSelectorExists('#scan-input'); $this->client->request('GET', '/inventory/sw.js'); self::assertResponseIsSuccessful(); self::assertResponseHeaderSame('Content-Type', 'text/javascript; charset=UTF-8'); self::assertStringContainsString('"/js/inventory.js"', (string)$this->client->getResponse()->getContent()); } public function testBookListContainsStandardNumbersAndPlacement(): void { $this->createBook(2, 'Solaris', 'Stanisław Lem')->setIsbn('9788373843455')->setIssn('9770867041003')->setPlacement('HOL/4/1'); $this->createBook(1, 'Plain')->lendTo('A Reader', new \DateTimeImmutable()); $this->entityManager()->flush(); $this->client->request('GET', '/inventory/books'); self::assertResponseIsSuccessful(); self::assertSame(['books' => [ ['id' => 1, 'title' => 'Plain', 'author' => 'Author', 'placement' => null, 'lastSeen' => null, 'borrowed' => true, 'isbn' => [], 'issn' => []], ['id' => 2, 'title' => 'Solaris', 'author' => 'Stanisław Lem', 'placement' => 'HOL/4/1', 'lastSeen' => null, 'borrowed' => false, 'isbn' => ['9788373843455'], 'issn' => ['9770867041003']], ]], $this->jsonResponse()); } public function testSyncAssignsLocationsAndMarksBooksAsSeen(): void { $this->createBook(1)->setPlacement('OLD/1'); $this->createBook(2); $before = new \DateTimeImmutable('-1 second'); $this->sync(['1' => 'HOL/4/1', '2' => ' Attic ', '99' => 'X', '3' => '']); self::assertResponseIsSuccessful(); self::assertSame(['results' => [ '1' => 'saved', '2' => 'saved', '99' => 'unknown_book', '3' => 'invalid', ]], $this->jsonResponse()); $book = $this->findBook(1); self::assertSame('HOL/4/1', $book?->getPlacement()); self::assertGreaterThanOrEqual($before, $book?->getLastSeen()); self::assertSame('Attic', $this->findBook(2)?->getPlacement()); } public function testScanningABorrowedBookMarksItAsReturned(): void { $lentAt = new \DateTimeImmutable('2026-10-01 10:00'); $this->createBook(1)->lendTo('A Reader', $lentAt); $this->entityManager()->flush(); $this->entityManager()->clear(); $this->sync(['1' => 'HOL/4/1']); self::assertSame(['results' => ['1' => 'returned']], $this->jsonResponse()); $book = $this->findBook(1); self::assertFalse($book?->isBorrowed()); self::assertSame('HOL/4/1', $book?->getPlacement()); $history = $book?->getBorrowHistory()->first(); self::assertSame('A Reader', $history ? $history->getWho() : null); self::assertEquals($lentAt, $history ? $history->getAt() : null); self::assertEquals($book?->getLastSeen(), $history ? $history->getTill() : null); $this->sync(['1' => 'HOL/4/1']); self::assertSame(['results' => ['1' => 'saved']], $this->jsonResponse()); self::assertCount(1, $this->findBook(1)?->getBorrowHistory() ?? []); } public function testSyncAcceptsBookZero(): void { $this->createBook(0); $this->sync((object)['0' => 'HOL/1/1']); self::assertResponseIsSuccessful(); self::assertSame('HOL/1/1', $this->findBook(0)?->getPlacement()); } public function testSyncRejectsAnythingButAJsonMap(): void { $this->createBook(1); $this->client->request('POST', '/inventory/sync', ['1' => 'HOL/4/1']); self::assertResponseStatusCodeSame(415); $this->sync([['id' => 1, 'location' => 'HOL/4/1']]); self::assertResponseStatusCodeSame(400); $this->sync(['1' => ['nested']]); self::assertResponseStatusCodeSame(400); self::assertNull($this->findBook(1)?->getPlacement()); } public function testDetailsSayWhenBookWasNeverSeen(): void { $this->createBook(1); $this->client->request('GET', '/book/1'); self::assertSelectorTextContains('.book-last-seen td', 'not seen during an inventory yet'); } public function testOustingKeepsLastSeen(): void { $this->createBook(1); $this->sync(['1' => 'HOL/4/1']); $seen = $this->findBook(1)?->getLastSeen(); $crawler = $this->client->request('GET', '/book/1'); self::assertSelectorTextContains('.book-last-seen td', (string)$seen?->format('Y-m-d H:i')); $this->client->submit($crawler->selectButton('Take out')->form(['oust[reason]' => 'Lost'])); $this->entityManager()->clear(); self::assertEquals($seen, $this->entityManager()->find(OustedBook::class, 1)?->getLastSeen()); $this->client->request('GET', '/ousted/1'); self::assertSelectorTextContains('.book-last-seen td', (string)$seen?->format('Y-m-d H:i')); } /** * @param array|object $body */ private function sync(array|object $body): void { $this->client->request('POST', '/inventory/sync', server: ['CONTENT_TYPE' => 'application/json'], content: json_encode($body, \JSON_THROW_ON_ERROR)); } /** * @return array */ private function jsonResponse(): array { return json_decode((string)$this->client->getResponse()->getContent(), true, flags: \JSON_THROW_ON_ERROR); } }