Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
108 / 108
100.00% covered (success)
100.00%
3 / 3
CRAP
n/a
0 / 0
givenLoanExists
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
givenUserExistsForLoan
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
givenBookExistsForLoan
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3// ╔════════════════════════════════════════════════════════════╗
4// ║ MIT Licence (#Expat) - https://opensource.org/licenses/MIT ║
5// ║ Copyright 2026 Frederic Poeydomenge <dyno@phexium.com>     ║
6// ╚════════════════════════════════════════════════════════════╝
7
8declare(strict_types=1);
9
10use AppDemo\Loan\Domain\Loan;
11use Phexium\Domain\Id\TimestampId;
12use Phexium\Domain\Specification\AlwaysFalseSpecification;
13use Phexium\Domain\Specification\AlwaysTrueSpecification;
14use Tests\AppDemo\Fixture\BookMother;
15use Tests\AppDemo\Fixture\LoanMother;
16use Tests\AppDemo\Fixture\UserMother;
17
18if (!function_exists('givenLoanExists')) {
19    function givenLoanExists(object $repository, int $loanId, int $userId, int $bookId, string $borrowedAt = '2025-01-01'): void
20    {
21        $repository->save(
22            LoanMother::create(id: $loanId, userId: $userId, bookId: $bookId, borrowedAt: $borrowedAt, dueAt: '2025-01-15')
23        );
24    }
25}
26
27if (!function_exists('givenUserExistsForLoan')) {
28    function givenUserExistsForLoan(object $repository, int $id, string $email): void
29    {
30        $repository->save(
31            UserMother::create(id: $id, email: $email)
32        );
33    }
34}
35
36if (!function_exists('givenBookExistsForLoan')) {
37    function givenBookExistsForLoan(object $repository, int $id, string $isbn): void
38    {
39        $repository->save(
40            BookMother::create(id: $id, title: 'Title'.$id, author: 'Author'.$id, isbn: $isbn)
41        );
42    }
43}
44
45describe('Persistence', function (): void {
46    it('saves and retrieves an active loan', function (): void {
47        $loanId = 12345;
48        $userId = 400;
49        $bookId = 567;
50        givenLoanExists($this->repository, $loanId, $userId, $bookId);
51
52        $foundLoan = $this->repository->getById(new TimestampId($loanId));
53
54        expect($foundLoan)->not->toBeNull()
55            ->and($foundLoan->getId()->getValue())->toBe($loanId)
56            ->and($foundLoan->getUserId()->getValue())->toBe($userId)
57            ->and($foundLoan->getBookId()->getValue())->toBe($bookId)
58        ;
59    });
60
61    it('throws exception for non-existing id via getById', function (): void {
62        expect(fn () => $this->repository->getById(new TimestampId(99999)))->toThrow(InvalidArgumentException::class);
63    });
64
65    it('checks for loan existence', function (): void {
66        givenLoanExists($this->repository, 12345, 400, 567);
67
68        expect($this->repository->exists(new TimestampId(12345)))->toBeTrue();
69        expect($this->repository->exists(new TimestampId(99999)))->toBeFalse();
70    });
71});
72
73describe('Querying', function (): void {
74    it('returns all loans via findAll', function (): void {
75        givenLoanExists($this->repository, 1001, 400, 567);
76        givenLoanExists($this->repository, 1002, 400, 567);
77
78        expect($this->repository->findAll())->toHaveCount(2);
79    });
80
81    it('returns matching loans via findBy', function (): void {
82        givenLoanExists($this->repository, 1001, 400, 567);
83        givenLoanExists($this->repository, 1002, 400, 567);
84
85        expect($this->repository->findBy(new AlwaysTrueSpecification()))->toHaveCount(2);
86        expect($this->repository->findBy(new AlwaysFalseSpecification()))->toHaveCount(0);
87    });
88
89    it('returns loan or null via findById', function (): void {
90        givenLoanExists($this->repository, 12345, 400, 567);
91
92        expect($this->repository->findById(new TimestampId(12345)))->toBeInstanceOf(Loan::class);
93        expect($this->repository->findById(new TimestampId(99999)))->toBeNull();
94    });
95
96    it('returns first matching loan or null via findOneBy', function (): void {
97        givenLoanExists($this->repository, 12345, 400, 567);
98
99        expect($this->repository->findOneBy(new AlwaysTrueSpecification()))->toBeInstanceOf(Loan::class);
100        expect($this->repository->findOneBy(new AlwaysFalseSpecification()))->toBeNull();
101    });
102
103    it('returns loans for a specific user via findByUserId', function (): void {
104        $userId = new TimestampId(400);
105
106        givenLoanExists($this->repository, 1001, 400, 567, '2025-01-01');
107        givenLoanExists($this->repository, 1002, 400, 678, '2025-02-02');
108
109        $loansCollection = $this->repository->findByUserId($userId);
110
111        expect($loansCollection)->toHaveCount(2)
112            ->and($loansCollection->first()->getBookId()->getValue())->toBe(678)
113            ->and($loansCollection->last()->getBookId()->getValue())->toBe(567)
114        ;
115    });
116
117    it('returns empty collection for user with no loans via findByUserId', function (): void {
118        $userId = new TimestampId(400);
119
120        $loansCollection = $this->repository->findByUserId($userId);
121
122        expect($loansCollection)->toHaveCount(0);
123    });
124
125    it('returns loans with user and book details via findAllWithDetails', function (): void {
126        givenUserExistsForLoan($this->userRepository, 400, 'john@example.com');
127        givenBookExistsForLoan($this->bookRepository, 567, '9780134494166');
128        givenLoanExists($this->repository, 1001, 400, 567);
129
130        $loansWithDetails = $this->repository->findAllWithDetails();
131
132        expect($loansWithDetails)->toHaveCount(1)
133            ->and($loansWithDetails[0]->userEmail)->toBe('john@example.com')
134            ->and($loansWithDetails[0]->bookTitle)->toBe('Title567')
135        ;
136    });
137
138    it('returns loans with book details for a user via findByUserIdWithBookDetails', function (): void {
139        givenUserExistsForLoan($this->userRepository, 400, 'john@example.com');
140        givenBookExistsForLoan($this->bookRepository, 567, '9780134494166');
141        givenLoanExists($this->repository, 1001, 400, 567);
142
143        $loansWithDetails = $this->repository->findByUserIdWithBookDetails(new TimestampId(400));
144
145        expect($loansWithDetails)->toHaveCount(1)
146            ->and($loansWithDetails[0]->bookTitle)->toBe('Title567')
147        ;
148    });
149});
150
151describe('Deletion', function (): void {
152    it('deletes a loan and returns affected rows', function (): void {
153        givenLoanExists($this->repository, 12345, 400, 567);
154        $loan = $this->repository->getById(new TimestampId(12345));
155
156        $affectedRows = $this->repository->delete($loan);
157
158        expect($affectedRows)->toBe(1)
159            ->and($this->repository->exists(new TimestampId(12345)))->toBeFalse()
160        ;
161    });
162
163    it('deletes a loan by id and returns affected rows', function (): void {
164        givenLoanExists($this->repository, 12345, 400, 567);
165
166        $affectedRows = $this->repository->deleteById(new TimestampId(12345));
167
168        expect($affectedRows)->toBe(1)
169            ->and($this->repository->exists(new TimestampId(12345)))->toBeFalse()
170        ;
171    });
172});