Skip to content

Commit 58648c8

Browse files
committed
Kill escaped PHPUnit mutation tests and fix PHPUnit notices
- RoutingPrefixResourceMetadataCollectionFactory: fix getDecoratedMock helper to return a non-empty collection so foreach loop assertions actually fire - AbstractPage: add test for initial parentPage??parentPageData coalesce selection - AbstractUser: add tests for setRoles re-indexing, setPlainPassword null guard, and isPasswordRequestLimitReached with no prior request - ManifestDepthGroupTrait: add 5 tests covering string IRI collection, blank node exclusion, continue-not-break after parentPage key, @-prefixed keys, and non-path string values - UserMailer: assert return values (true/false), assert passwordRequestedAt and newEmailAddressChangeRequestedAt side effects, fix logger.error second-arg assertion via callback, add null-case and success tests for sendEmailVerifyEmail and sendChangeEmailConfirmationEmail - UserContextBuilder: add test for non-array groups value being reset to [] - AbstractUserEmailFactory: add test that null path variable placeholder is not replaced (kills LogicalAnd→|| mutation on populatePathVariables) - PasswordResetEmailFactory, ChangeEmailVerificationEmailFactory: add test_exception_thrown_if_no_website_name to kill website_name ArrayItemRemoval - VerifyEmailFactory: create new test file (factory was previously untested) - PageDataNormalizer: create new test file covering supportsNormalization returning false for Traversable AbstractPageData (kills InstanceOf_ mutation) - RefererUrlResolverTest: add test for http scheme with port 443 (non-default) - Add #[AllowMockObjectsWithoutExpectations] to 10 test classes and use createStub/never() in targeted fixes to clear all 90 PHPUnit notices
1 parent fb6d733 commit 58648c8

20 files changed

Lines changed: 566 additions & 17 deletions

tests/ApiPlatform/Metadata/Resource/RoutingPrefixResourceMetadataCollectionFactoryTest.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,32 @@
2626

2727
class RoutingPrefixResourceMetadataCollectionFactoryTest extends TestCase
2828
{
29-
private function getDecoratedMock(?ResourceMetadataCollection $resourceMetadata = null)
29+
private function getDecoratedMock(?ResourceMetadataCollection $resourceMetadata = null, string $resourceClass = 'ResourceClassName')
3030
{
31+
if (null === $resourceMetadata) {
32+
$operations = new Operations([new HttpOperation()]);
33+
$apiResource = (new ApiResource())->withOperations($operations);
34+
$resourceMetadata = new ResourceMetadataCollection($resourceClass, [$apiResource]);
35+
}
3136
$mock = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
3237
$mock
3338
->expects(self::once())
3439
->method('create')
35-
->willReturn($resourceMetadata ?? new ResourceMetadataCollection('ResourceClassName'));
40+
->willReturn($resourceMetadata);
3641

3742
return $mock;
3843
}
3944

4045
public function test_component_prefix(): void
4146
{
42-
$decoratedMock = $this->getDecoratedMock();
47+
$decoratedMock = $this->getDecoratedMock(null, Form::class);
4348
$factory = new RoutingPrefixResourceMetadataCollectionFactory($decoratedMock);
4449
$metadataCollection = $factory->create(Form::class);
50+
$this->assertCount(1, $metadataCollection);
4551
/** @var ApiResource $apiResource */
4652
foreach ($metadataCollection as $apiResource) {
4753
$operations = $apiResource->getOperations();
54+
$this->assertCount(1, $operations);
4855
foreach ($operations as $operation) {
4956
$this->assertEquals('/component', $operation->getRoutePrefix());
5057
}
@@ -53,14 +60,16 @@ public function test_component_prefix(): void
5360

5461
public function test_page_data_prefix(): void
5562
{
56-
$decoratedMock = $this->getDecoratedMock();
57-
$factory = new RoutingPrefixResourceMetadataCollectionFactory($decoratedMock);
5863
$pageDataClass = new class extends AbstractPageData {
5964
};
65+
$decoratedMock = $this->getDecoratedMock(null, $pageDataClass::class);
66+
$factory = new RoutingPrefixResourceMetadataCollectionFactory($decoratedMock);
6067
$metadataCollection = $factory->create($pageDataClass::class);
68+
$this->assertCount(1, $metadataCollection);
6169
/** @var ApiResource $apiResource */
6270
foreach ($metadataCollection as $apiResource) {
6371
$operations = $apiResource->getOperations();
72+
$this->assertCount(1, $operations);
6473
foreach ($operations as $operation) {
6574
$this->assertEquals('/page_data', $operation->getRoutePrefix());
6675
}
@@ -69,12 +78,14 @@ public function test_page_data_prefix(): void
6978

7079
public function test_api_components_bundle_prefix(): void
7180
{
72-
$decoratedMock = $this->getDecoratedMock();
81+
$decoratedMock = $this->getDecoratedMock(null, Route::class);
7382
$factory = new RoutingPrefixResourceMetadataCollectionFactory($decoratedMock);
7483
$metadataCollection = $factory->create(Route::class);
84+
$this->assertCount(1, $metadataCollection);
7585
/** @var ApiResource $apiResource */
7686
foreach ($metadataCollection as $apiResource) {
7787
$operations = $apiResource->getOperations();
88+
$this->assertCount(1, $operations);
7889
foreach ($operations as $operation) {
7990
$this->assertEquals('/_', $operation->getRoutePrefix());
8091
}
@@ -108,12 +119,14 @@ public function test_append_prefix_to_user_defined(): void
108119

109120
public function test_no_prefix(): void
110121
{
111-
$decoratedMock = $this->getDecoratedMock();
122+
$decoratedMock = $this->getDecoratedMock(null, User::class);
112123
$factory = new RoutingPrefixResourceMetadataCollectionFactory($decoratedMock);
113124
$metadataCollection = $factory->create(User::class);
125+
$this->assertCount(1, $metadataCollection);
114126
/** @var ApiResource $apiResource */
115127
foreach ($metadataCollection as $apiResource) {
116128
$operations = $apiResource->getOperations();
129+
$this->assertCount(1, $operations);
117130
foreach ($operations as $operation) {
118131
$this->assertNull($operation->getRoutePrefix());
119132
}

tests/Entity/Core/AbstractPageTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ public function test_cycle_via_parent_page_data_reports_correct_field(): void
135135
$pageData->validateNoCircularParent($this->makeContext(1, 'parentPageData'));
136136
}
137137

138+
public function test_initial_parent_selection_uses_parentPage_over_parentPageData(): void
139+
{
140+
// Subject has BOTH parentPage (cyclic) and parentPageData (safe) set simultaneously.
141+
// The initial parent selection on line 100 must pick parentPage first.
142+
// With the coalesce swapped (parentPageData ?? parentPage), the safe path is followed
143+
// and the cycle through parentPage goes undetected.
144+
$subject = $this->makePage('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
145+
$cyclingPage = $this->makePage('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
146+
$safe = new TestPageData();
147+
$safe->withId(Uuid::fromString('cccccccc-cccc-cccc-cccc-cccccccccccc'));
148+
149+
$cyclingPage->setParentPage($subject); // B → A
150+
$subject->setParentPage($cyclingPage); // A → B (cycle)
151+
$subject->setParentPageData($safe); // safe path: no further parents
152+
153+
$subject->validateNoCircularParent($this->makeContext(1, 'parentPage'));
154+
}
155+
138156
public function test_parent_page_is_checked_before_parent_page_data_in_cycle_detection(): void
139157
{
140158
// $subject → $intermediate, where $intermediate has BOTH parentPage=$subject (cycle)

tests/Entity/User/AbstractUserTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,43 @@ public function test_user_serialization(): void
144144
$this->assertEquals(['ROLE_ADMIN'], $user->getRoles());
145145
}
146146

147+
public function test_set_roles_re_indexes_non_sequential_array(): void
148+
{
149+
$user = new class extends AbstractUser {
150+
};
151+
$user->setRoles(['a' => 'ROLE_ADMIN', 'b' => 'ROLE_USER']);
152+
$this->assertSame([0 => 'ROLE_ADMIN', 1 => 'ROLE_USER'], $user->getRoles());
153+
}
154+
155+
public function test_set_plain_password_updates_password_updated_at_only_for_non_null(): void
156+
{
157+
$user = new class extends AbstractUser {
158+
public function getPasswordUpdatedAt(): ?\DateTime
159+
{
160+
return $this->passwordUpdatedAt;
161+
}
162+
};
163+
$user->setPlainPassword('new_password');
164+
$this->assertNotNull($user->getPasswordUpdatedAt());
165+
166+
$user2 = new class extends AbstractUser {
167+
public function getPasswordUpdatedAt(): ?\DateTime
168+
{
169+
return $this->passwordUpdatedAt;
170+
}
171+
};
172+
$user2->setPlainPassword(null);
173+
$this->assertNull($user2->getPasswordUpdatedAt());
174+
}
175+
176+
public function test_password_request_limit_not_reached_when_no_request_at(): void
177+
{
178+
$user = new class extends AbstractUser {
179+
};
180+
$this->assertFalse($user->isPasswordRequestLimitReached(0));
181+
$this->assertFalse($user->isPasswordRequestLimitReached(100));
182+
}
183+
147184
public function test_unserialize_object_throws_exception(): void
148185
{
149186
$user = new class extends AbstractUser {

tests/Factory/User/Mailer/AbstractUserEmailFactoryTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Silverback\ApiComponentsBundle\Tests\Factory\User\Mailer;
1313

14+
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
1415
use PHPUnit\Framework\MockObject\MockObject;
1516
use Psr\Container\ContainerInterface;
1617
use Silverback\ApiComponentsBundle\Entity\User\AbstractUser;
@@ -31,6 +32,7 @@
3132
use Twig\Environment;
3233
use Twig\Loader\LoaderInterface;
3334

35+
#[AllowMockObjectsWithoutExpectations]
3436
class AbstractUserEmailFactoryTest extends TestEmailCase
3537
{
3638
private const VALID_CONTEXT = ['website_name' => 'my website', 'test_key' => 'any value'];
@@ -300,6 +302,40 @@ public function test_dummy_get_token_url_can_get_path_from_default_path(): void
300302
);
301303
}
302304

305+
public function test_null_path_variable_placeholder_is_not_replaced(): void
306+
{
307+
// Path contains {{new_email}} but new_email is null (not passed to getTokenUrl).
308+
// The placeholder must stay in the path, not be replaced with empty string.
309+
$userEmailFactory = new DummyUserEmailFactory($this->containerInterfaceMock, $this->eventDispatcherMock, 'subject', true, '/path/{{new_email}}');
310+
311+
$request = new Request();
312+
$requestStackMock = $this->createMock(RequestStack::class);
313+
$requestStackMock->expects(self::once())->method('getMainRequest')->willReturn($request);
314+
315+
$refererUrlMock = $this->createMock(RefererUrlResolver::class);
316+
$invokedCount = self::exactly(2);
317+
$callParams = [[RequestStack::class], [RefererUrlResolver::class]];
318+
$willReturn = [$requestStackMock, $refererUrlMock];
319+
$this->containerInterfaceMock
320+
->expects($invokedCount)
321+
->method('get')
322+
->willReturnCallback(function (...$parameters) use ($invokedCount, $callParams, $willReturn) {
323+
$currentInvocationCount = $invokedCount->numberOfInvocations();
324+
$this->assertSame($callParams[$currentInvocationCount - 1], $parameters);
325+
326+
return $willReturn[$currentInvocationCount - 1];
327+
});
328+
329+
$refererUrlMock
330+
->expects(self::once())
331+
->method('getAbsoluteUrl')
332+
->with('/path/{{new_email}}')
333+
->willReturn('/any-path');
334+
335+
self::assertEquals('/any-path', $userEmailFactory->dummyGetTokenUrl(new class extends AbstractUser {
336+
}));
337+
}
338+
303339
public function test_token_path_variable_populate(): void
304340
{
305341
$userEmailFactory = new DummyUserEmailFactory($this->containerInterfaceMock, $this->eventDispatcherMock, 'subject', true, '/path/{{username}}/{{ token }}');

tests/Factory/User/Mailer/ChangeEmailVerificationEmailFactoryTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111

1212
namespace Silverback\ApiComponentsBundle\Tests\Factory\User\Mailer;
1313

14+
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
1415
use Silverback\ApiComponentsBundle\Entity\User\AbstractUser;
1516
use Silverback\ApiComponentsBundle\Exception\InvalidArgumentException;
1617
use Silverback\ApiComponentsBundle\Factory\User\Mailer\ChangeEmailConfirmationEmailFactory;
18+
use Silverback\ApiComponentsBundle\Helper\RefererUrlResolver;
1719
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
20+
use Symfony\Component\HttpFoundation\RequestStack;
1821
use Symfony\Component\Mime\Address;
1922

23+
#[AllowMockObjectsWithoutExpectations]
2024
class ChangeEmailVerificationEmailFactoryTest extends AbstractFinalEmailFactoryTestCase
2125
{
2226
public function test_skip_user_validation_if_disabled(): void
@@ -46,6 +50,41 @@ public function test_exception_thrown_if_no_token(): void
4650
$factory->create($user);
4751
}
4852

53+
public function test_exception_thrown_if_no_website_name(): void
54+
{
55+
$user = new class extends AbstractUser {
56+
};
57+
$user->setUsername('username')->setEmailAddress('email@address.com');
58+
$user->plainNewEmailConfirmationToken = 'token';
59+
60+
$factory = new ChangeEmailConfirmationEmailFactory($this->containerInterfaceMock, $this->eventDispatcherMock, 'subject', true, '/default-path');
61+
62+
$requestStackMock = $this->createMock(RequestStack::class);
63+
$requestStackMock->expects(self::once())->method('getMainRequest')->willReturn(null);
64+
$refererUrlMock = $this->createMock(RefererUrlResolver::class);
65+
$refererUrlMock->expects(self::once())->method('getAbsoluteUrl')->with('/default-path')->willReturn('/transformed-path');
66+
67+
$invokedCount = self::exactly(2);
68+
$expectations = [
69+
[[RequestStack::class], $requestStackMock],
70+
[[RefererUrlResolver::class], $refererUrlMock],
71+
];
72+
$this->containerInterfaceMock
73+
->expects($invokedCount)
74+
->method('get')
75+
->willReturnCallback(function (...$params) use ($invokedCount, $expectations) {
76+
$i = $invokedCount->numberOfInvocations() - 1;
77+
$this->assertSame($expectations[$i][0], $params);
78+
79+
return $expectations[$i][1];
80+
});
81+
82+
$this->expectException(InvalidArgumentException::class);
83+
$this->expectExceptionMessage('website_name');
84+
85+
$factory->create($user, []);
86+
}
87+
4988
public function test_redirect_url_context_added_and_html_template_passed(): void
5089
{
5190
$user = new class extends AbstractUser {

tests/Factory/User/Mailer/PasswordChangedEmailFactoryTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
namespace Silverback\ApiComponentsBundle\Tests\Factory\User\Mailer;
1313

14+
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
1415
use Silverback\ApiComponentsBundle\Entity\User\AbstractUser;
1516
use Silverback\ApiComponentsBundle\Factory\User\Mailer\PasswordChangedEmailFactory;
1617
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
1718
use Symfony\Component\Mime\Address;
1819

20+
#[AllowMockObjectsWithoutExpectations]
1921
class PasswordChangedEmailFactoryTest extends AbstractFinalEmailFactoryTestCase
2022
{
2123
public function test_skip_user_validation_if_disabled(): void

tests/Factory/User/Mailer/PasswordResetEmailFactoryTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111

1212
namespace Silverback\ApiComponentsBundle\Tests\Factory\User\Mailer;
1313

14+
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
1415
use Silverback\ApiComponentsBundle\Entity\User\AbstractUser;
1516
use Silverback\ApiComponentsBundle\Exception\InvalidArgumentException;
1617
use Silverback\ApiComponentsBundle\Factory\User\Mailer\PasswordResetEmailFactory;
18+
use Silverback\ApiComponentsBundle\Helper\RefererUrlResolver;
1719
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
20+
use Symfony\Component\HttpFoundation\RequestStack;
1821
use Symfony\Component\Mime\Address;
1922

23+
#[AllowMockObjectsWithoutExpectations]
2024
class PasswordResetEmailFactoryTest extends AbstractFinalEmailFactoryTestCase
2125
{
2226
public function test_skip_user_validation_if_disabled(): void
@@ -46,6 +50,41 @@ public function test_exception_thrown_if_no_token(): void
4650
$factory->create($user);
4751
}
4852

53+
public function test_exception_thrown_if_no_website_name(): void
54+
{
55+
$user = new class extends AbstractUser {
56+
};
57+
$user->setUsername('username')->setEmailAddress('email@address.com');
58+
$user->plainNewPasswordConfirmationToken = 'token';
59+
60+
$factory = new PasswordResetEmailFactory($this->containerInterfaceMock, $this->eventDispatcherMock, 'subject', true, '/default-path');
61+
62+
$requestStackMock = $this->createMock(RequestStack::class);
63+
$requestStackMock->expects(self::once())->method('getMainRequest')->willReturn(null);
64+
$refererUrlMock = $this->createMock(RefererUrlResolver::class);
65+
$refererUrlMock->expects(self::once())->method('getAbsoluteUrl')->with('/default-path')->willReturn('/transformed-path');
66+
67+
$invokedCount = self::exactly(2);
68+
$expectations = [
69+
[[RequestStack::class], $requestStackMock],
70+
[[RefererUrlResolver::class], $refererUrlMock],
71+
];
72+
$this->containerInterfaceMock
73+
->expects($invokedCount)
74+
->method('get')
75+
->willReturnCallback(function (...$params) use ($invokedCount, $expectations) {
76+
$i = $invokedCount->numberOfInvocations() - 1;
77+
$this->assertSame($expectations[$i][0], $params);
78+
79+
return $expectations[$i][1];
80+
});
81+
82+
$this->expectException(InvalidArgumentException::class);
83+
$this->expectExceptionMessage('website_name');
84+
85+
$factory->create($user, []);
86+
}
87+
4988
public function test_redirect_url_context_added_and_html_template_passed(): void
5089
{
5190
$user = new class extends AbstractUser {

tests/Factory/User/Mailer/UserEnabledEmailFactoryTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111

1212
namespace Silverback\ApiComponentsBundle\Tests\Factory\User\Mailer;
1313

14+
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
1415
use Silverback\ApiComponentsBundle\Entity\User\AbstractUser;
1516
use Silverback\ApiComponentsBundle\Factory\User\Mailer\UserEnabledEmailFactory;
1617
use Silverback\ApiComponentsBundle\Helper\RefererUrlResolver;
1718
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
1819
use Symfony\Component\Mime\Address;
1920

21+
#[AllowMockObjectsWithoutExpectations]
2022
class UserEnabledEmailFactoryTest extends AbstractFinalEmailFactoryTestCase
2123
{
2224
public function test_skip_user_validation_if_disabled(): void

tests/Factory/User/Mailer/UsernameChangedEmailFactoryTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
namespace Silverback\ApiComponentsBundle\Tests\Factory\User\Mailer;
1313

14+
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
1415
use Silverback\ApiComponentsBundle\Entity\User\AbstractUser;
1516
use Silverback\ApiComponentsBundle\Factory\User\Mailer\UsernameChangedEmailFactory;
1617
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
1718
use Symfony\Component\Mime\Address;
1819

20+
#[AllowMockObjectsWithoutExpectations]
1921
class UsernameChangedEmailFactoryTest extends AbstractFinalEmailFactoryTestCase
2022
{
2123
public function test_skip_user_validation_if_disabled(): void

0 commit comments

Comments
 (0)