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